diff -r 000000000000 -r 4f2f89ce4247 WebCore/editing/Editor.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebCore/editing/Editor.cpp Fri Sep 17 09:02:29 2010 +0300 @@ -0,0 +1,2894 @@ +/* + * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved. + * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "Editor.h" + +#include "AXObjectCache.h" +#include "ApplyStyleCommand.h" +#include "CSSComputedStyleDeclaration.h" +#include "CSSMutableStyleDeclaration.h" +#include "CSSProperty.h" +#include "CSSPropertyNames.h" +#include "CSSValueKeywords.h" +#include "CharacterNames.h" +#include "ClipboardEvent.h" +#include "CompositionEvent.h" +#include "CreateLinkCommand.h" +#include "DeleteButtonController.h" +#include "DeleteSelectionCommand.h" +#include "DocLoader.h" +#include "DocumentFragment.h" +#include "EditorClient.h" +#include "EventHandler.h" +#include "EventNames.h" +#include "FocusController.h" +#include "Frame.h" +#include "FrameTree.h" +#include "FrameView.h" +#include "HTMLInputElement.h" +#include "HTMLTextAreaElement.h" +#include "HitTestResult.h" +#include "IndentOutdentCommand.h" +#include "InsertListCommand.h" +#include "KeyboardEvent.h" +#include "KillRing.h" +#include "ModifySelectionListLevel.h" +#include "Page.h" +#include "Pasteboard.h" +#include "RemoveFormatCommand.h" +#include "RenderBlock.h" +#include "RenderPart.h" +#include "ReplaceSelectionCommand.h" +#include "Settings.h" +#include "Sound.h" +#include "Text.h" +#include "TextIterator.h" +#include "TypingCommand.h" +#include "htmlediting.h" +#include "markup.h" +#include "visible_units.h" +#include + +namespace WebCore { + +using namespace std; +using namespace HTMLNames; + +// When an event handler has moved the selection outside of a text control +// we should use the target control's selection for this editing operation. +VisibleSelection Editor::selectionForCommand(Event* event) +{ + VisibleSelection selection = m_frame->selection()->selection(); + if (!event) + return selection; + // If the target is a text control, and the current selection is outside of its shadow tree, + // then use the saved selection for that text control. + Node* target = event->target()->toNode(); + Node* selectionStart = selection.start().node(); + if (target && (!selectionStart || target->shadowAncestorNode() != selectionStart->shadowAncestorNode())) { + if (target->hasTagName(inputTag) && static_cast(target)->isTextField()) + return static_cast(target)->selection(); + if (target->hasTagName(textareaTag)) + return static_cast(target)->selection(); + } + return selection; +} + +// Function considers Mac editing behavior a fallback when Page or Settings is not available. +EditingBehavior Editor::behavior() const +{ + if (!m_frame || !m_frame->settings()) + return EditingBehavior(EditingMacBehavior); + + return EditingBehavior(m_frame->settings()->editingBehaviorType()); +} + +EditorClient* Editor::client() const +{ + if (Page* page = m_frame->page()) + return page->editorClient(); + return 0; +} + +void Editor::handleKeyboardEvent(KeyboardEvent* event) +{ + if (EditorClient* c = client()) + c->handleKeyboardEvent(event); +} + +void Editor::handleInputMethodKeydown(KeyboardEvent* event) +{ + if (EditorClient* c = client()) + c->handleInputMethodKeydown(event); +} + +bool Editor::canEdit() const +{ + return m_frame->selection()->isContentEditable(); +} + +bool Editor::canEditRichly() const +{ + return m_frame->selection()->isContentRichlyEditable(); +} + +// WinIE uses onbeforecut and onbeforepaste to enables the cut and paste menu items. They +// also send onbeforecopy, apparently for symmetry, but it doesn't affect the menu items. +// We need to use onbeforecopy as a real menu enabler because we allow elements that are not +// normally selectable to implement copy/paste (like divs, or a document body). + +bool Editor::canDHTMLCut() +{ + return !m_frame->selection()->isInPasswordField() && !dispatchCPPEvent(eventNames().beforecutEvent, ClipboardNumb); +} + +bool Editor::canDHTMLCopy() +{ + return !m_frame->selection()->isInPasswordField() && !dispatchCPPEvent(eventNames().beforecopyEvent, ClipboardNumb); +} + +bool Editor::canDHTMLPaste() +{ + return !dispatchCPPEvent(eventNames().beforepasteEvent, ClipboardNumb); +} + +bool Editor::canCut() const +{ + return canCopy() && canDelete(); +} + +static HTMLImageElement* imageElementFromImageDocument(Document* document) +{ + if (!document) + return 0; + if (!document->isImageDocument()) + return 0; + + HTMLElement* body = document->body(); + if (!body) + return 0; + + Node* node = body->firstChild(); + if (!node) + return 0; + if (!node->hasTagName(imgTag)) + return 0; + return static_cast(node); +} + +bool Editor::canCopy() const +{ + if (imageElementFromImageDocument(m_frame->document())) + return true; + SelectionController* selection = m_frame->selection(); + return selection->isRange() && !selection->isInPasswordField(); +} + +bool Editor::canPaste() const +{ + return canEdit(); +} + +bool Editor::canDelete() const +{ + SelectionController* selection = m_frame->selection(); + return selection->isRange() && selection->isContentEditable(); +} + +bool Editor::canDeleteRange(Range* range) const +{ + ExceptionCode ec = 0; + Node* startContainer = range->startContainer(ec); + Node* endContainer = range->endContainer(ec); + if (!startContainer || !endContainer) + return false; + + if (!startContainer->isContentEditable() || !endContainer->isContentEditable()) + return false; + + if (range->collapsed(ec)) { + VisiblePosition start(startContainer, range->startOffset(ec), DOWNSTREAM); + VisiblePosition previous = start.previous(); + // FIXME: We sometimes allow deletions at the start of editable roots, like when the caret is in an empty list item. + if (previous.isNull() || previous.deepEquivalent().node()->rootEditableElement() != startContainer->rootEditableElement()) + return false; + } + return true; +} + +bool Editor::smartInsertDeleteEnabled() +{ + return client() && client()->smartInsertDeleteEnabled(); +} + +bool Editor::canSmartCopyOrDelete() +{ + return client() && client()->smartInsertDeleteEnabled() && m_frame->selectionGranularity() == WordGranularity; +} + +bool Editor::isSelectTrailingWhitespaceEnabled() +{ + return client() && client()->isSelectTrailingWhitespaceEnabled(); +} + +bool Editor::deleteWithDirection(SelectionController::EDirection direction, TextGranularity granularity, bool killRing, bool isTypingAction) +{ + if (!canEdit()) + return false; + + if (m_frame->selection()->isRange()) { + if (isTypingAction) { + TypingCommand::deleteKeyPressed(m_frame->document(), canSmartCopyOrDelete(), granularity); + revealSelectionAfterEditingOperation(); + } else { + if (killRing) + addToKillRing(selectedRange().get(), false); + deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); + // Implicitly calls revealSelectionAfterEditingOperation(). + } + } else { + switch (direction) { + case SelectionController::DirectionForward: + case SelectionController::DirectionRight: + TypingCommand::forwardDeleteKeyPressed(m_frame->document(), canSmartCopyOrDelete(), granularity, killRing); + break; + case SelectionController::DirectionBackward: + case SelectionController::DirectionLeft: + TypingCommand::deleteKeyPressed(m_frame->document(), canSmartCopyOrDelete(), granularity, killRing); + break; + } + revealSelectionAfterEditingOperation(); + } + + // FIXME: We should to move this down into deleteKeyPressed. + // clear the "start new kill ring sequence" setting, because it was set to true + // when the selection was updated by deleting the range + if (killRing) + setStartNewKillRingSequence(false); + + return true; +} + +void Editor::deleteSelectionWithSmartDelete(bool smartDelete) +{ + if (m_frame->selection()->isNone()) + return; + + applyCommand(DeleteSelectionCommand::create(m_frame->document(), smartDelete)); +} + +void Editor::pasteAsPlainTextWithPasteboard(Pasteboard* pasteboard) +{ + String text = pasteboard->plainText(m_frame); + if (client() && client()->shouldInsertText(text, selectedRange().get(), EditorInsertActionPasted)) + replaceSelectionWithText(text, false, canSmartReplaceWithPasteboard(pasteboard)); +} + +void Editor::pasteWithPasteboard(Pasteboard* pasteboard, bool allowPlainText) +{ + RefPtr range = selectedRange(); + bool chosePlainText; + RefPtr fragment = pasteboard->documentFragment(m_frame, range, allowPlainText, chosePlainText); + if (fragment && shouldInsertFragment(fragment, range, EditorInsertActionPasted)) + replaceSelectionWithFragment(fragment, false, canSmartReplaceWithPasteboard(pasteboard), chosePlainText); +} + +bool Editor::canSmartReplaceWithPasteboard(Pasteboard* pasteboard) +{ + return client() && client()->smartInsertDeleteEnabled() && pasteboard->canSmartReplace(); +} + +bool Editor::shouldInsertFragment(PassRefPtr fragment, PassRefPtr replacingDOMRange, EditorInsertAction givenAction) +{ + if (!client()) + return false; + + Node* child = fragment->firstChild(); + if (child && fragment->lastChild() == child && child->isCharacterDataNode()) + return client()->shouldInsertText(static_cast(child)->data(), replacingDOMRange.get(), givenAction); + + return client()->shouldInsertNode(fragment.get(), replacingDOMRange.get(), givenAction); +} + +void Editor::replaceSelectionWithFragment(PassRefPtr fragment, bool selectReplacement, bool smartReplace, bool matchStyle) +{ + if (m_frame->selection()->isNone() || !fragment) + return; + + applyCommand(ReplaceSelectionCommand::create(m_frame->document(), fragment, selectReplacement, smartReplace, matchStyle)); + revealSelectionAfterEditingOperation(); +} + +void Editor::replaceSelectionWithText(const String& text, bool selectReplacement, bool smartReplace) +{ + replaceSelectionWithFragment(createFragmentFromText(selectedRange().get(), text), selectReplacement, smartReplace, true); +} + +PassRefPtr Editor::selectedRange() +{ + if (!m_frame) + return 0; + return m_frame->selection()->toNormalizedRange(); +} + +bool Editor::shouldDeleteRange(Range* range) const +{ + ExceptionCode ec; + if (!range || range->collapsed(ec)) + return false; + + if (!canDeleteRange(range)) + return false; + + return client() && client()->shouldDeleteRange(range); +} + +bool Editor::tryDHTMLCopy() +{ + if (m_frame->selection()->isInPasswordField()) + return false; + + if (canCopy()) + // Must be done before oncopy adds types and data to the pboard, + // also done for security, as it erases data from the last copy/paste. + Pasteboard::generalPasteboard()->clear(); + + return !dispatchCPPEvent(eventNames().copyEvent, ClipboardWritable); +} + +bool Editor::tryDHTMLCut() +{ + if (m_frame->selection()->isInPasswordField()) + return false; + + if (canCut()) + // Must be done before oncut adds types and data to the pboard, + // also done for security, as it erases data from the last copy/paste. + Pasteboard::generalPasteboard()->clear(); + + return !dispatchCPPEvent(eventNames().cutEvent, ClipboardWritable); +} + +bool Editor::tryDHTMLPaste() +{ + return !dispatchCPPEvent(eventNames().pasteEvent, ClipboardReadable); +} + +void Editor::writeSelectionToPasteboard(Pasteboard* pasteboard) +{ + pasteboard->writeSelection(selectedRange().get(), canSmartCopyOrDelete(), m_frame); +} + +bool Editor::shouldInsertText(const String& text, Range* range, EditorInsertAction action) const +{ + return client() && client()->shouldInsertText(text, range, action); +} + +bool Editor::shouldShowDeleteInterface(HTMLElement* element) const +{ + return client() && client()->shouldShowDeleteInterface(element); +} + +void Editor::respondToChangedSelection(const VisibleSelection& oldSelection) +{ + if (client()) + client()->respondToChangedSelection(); + m_deleteButtonController->respondToChangedSelection(oldSelection); +} + +void Editor::respondToChangedContents(const VisibleSelection& endingSelection) +{ + if (AXObjectCache::accessibilityEnabled()) { + Node* node = endingSelection.start().node(); + if (node) + m_frame->document()->axObjectCache()->postNotification(node->renderer(), AXObjectCache::AXValueChanged, false); + } + + if (client()) + client()->respondToChangedContents(); +} + +const SimpleFontData* Editor::fontForSelection(bool& hasMultipleFonts) const +{ +#if !PLATFORM(QT) + hasMultipleFonts = false; + + if (!m_frame->selection()->isRange()) { + Node* nodeToRemove; + RenderStyle* style = m_frame->styleForSelectionStart(nodeToRemove); // sets nodeToRemove + + const SimpleFontData* result = 0; + if (style) + result = style->font().primaryFont(); + + if (nodeToRemove) { + ExceptionCode ec; + nodeToRemove->remove(ec); + ASSERT(!ec); + } + + return result; + } + + const SimpleFontData* font = 0; + + RefPtr range = m_frame->selection()->toNormalizedRange(); + Node* startNode = range->editingStartPosition().node(); + if (startNode) { + Node* pastEnd = range->pastLastNode(); + // In the loop below, n should eventually match pastEnd and not become nil, but we've seen at least one + // unreproducible case where this didn't happen, so check for nil also. + for (Node* n = startNode; n && n != pastEnd; n = n->traverseNextNode()) { + RenderObject* renderer = n->renderer(); + if (!renderer) + continue; + // FIXME: Are there any node types that have renderers, but that we should be skipping? + const SimpleFontData* f = renderer->style()->font().primaryFont(); + if (!font) + font = f; + else if (font != f) { + hasMultipleFonts = true; + break; + } + } + } + + return font; +#else + return 0; +#endif +} + +WritingDirection Editor::textDirectionForSelection(bool& hasNestedOrMultipleEmbeddings) const +{ + hasNestedOrMultipleEmbeddings = true; + + if (m_frame->selection()->isNone()) + return NaturalWritingDirection; + + Position pos = m_frame->selection()->selection().start().downstream(); + + Node* node = pos.node(); + if (!node) + return NaturalWritingDirection; + + Position end; + if (m_frame->selection()->isRange()) { + end = m_frame->selection()->selection().end().upstream(); + + Node* pastLast = Range::create(m_frame->document(), rangeCompliantEquivalent(pos), rangeCompliantEquivalent(end))->pastLastNode(); + for (Node* n = node; n && n != pastLast; n = n->traverseNextNode()) { + if (!n->isStyledElement()) + continue; + + RefPtr style = computedStyle(n); + RefPtr unicodeBidi = style->getPropertyCSSValue(CSSPropertyUnicodeBidi); + if (!unicodeBidi) + continue; + + ASSERT(unicodeBidi->isPrimitiveValue()); + int unicodeBidiValue = static_cast(unicodeBidi.get())->getIdent(); + if (unicodeBidiValue == CSSValueEmbed || unicodeBidiValue == CSSValueBidiOverride) + return NaturalWritingDirection; + } + } + + if (m_frame->selection()->isCaret()) { + if (CSSMutableStyleDeclaration *typingStyle = m_frame->typingStyle()) { + RefPtr unicodeBidi = typingStyle->getPropertyCSSValue(CSSPropertyUnicodeBidi); + if (unicodeBidi) { + ASSERT(unicodeBidi->isPrimitiveValue()); + int unicodeBidiValue = static_cast(unicodeBidi.get())->getIdent(); + if (unicodeBidiValue == CSSValueEmbed) { + RefPtr direction = typingStyle->getPropertyCSSValue(CSSPropertyDirection); + ASSERT(!direction || direction->isPrimitiveValue()); + if (direction) { + hasNestedOrMultipleEmbeddings = false; + return static_cast(direction.get())->getIdent() == CSSValueLtr ? LeftToRightWritingDirection : RightToLeftWritingDirection; + } + } else if (unicodeBidiValue == CSSValueNormal) { + hasNestedOrMultipleEmbeddings = false; + return NaturalWritingDirection; + } + } + } + node = m_frame->selection()->selection().visibleStart().deepEquivalent().node(); + } + + // The selection is either a caret with no typing attributes or a range in which no embedding is added, so just use the start position + // to decide. + Node* block = enclosingBlock(node); + WritingDirection foundDirection = NaturalWritingDirection; + + for (; node != block; node = node->parent()) { + if (!node->isStyledElement()) + continue; + + RefPtr style = computedStyle(node); + RefPtr unicodeBidi = style->getPropertyCSSValue(CSSPropertyUnicodeBidi); + if (!unicodeBidi) + continue; + + ASSERT(unicodeBidi->isPrimitiveValue()); + int unicodeBidiValue = static_cast(unicodeBidi.get())->getIdent(); + if (unicodeBidiValue == CSSValueNormal) + continue; + + if (unicodeBidiValue == CSSValueBidiOverride) + return NaturalWritingDirection; + + ASSERT(unicodeBidiValue == CSSValueEmbed); + RefPtr direction = style->getPropertyCSSValue(CSSPropertyDirection); + if (!direction) + continue; + + ASSERT(direction->isPrimitiveValue()); + int directionValue = static_cast(direction.get())->getIdent(); + if (directionValue != CSSValueLtr && directionValue != CSSValueRtl) + continue; + + if (foundDirection != NaturalWritingDirection) + return NaturalWritingDirection; + + // In the range case, make sure that the embedding element persists until the end of the range. + if (m_frame->selection()->isRange() && !end.node()->isDescendantOf(node)) + return NaturalWritingDirection; + + foundDirection = directionValue == CSSValueLtr ? LeftToRightWritingDirection : RightToLeftWritingDirection; + } + hasNestedOrMultipleEmbeddings = false; + return foundDirection; +} + +bool Editor::hasBidiSelection() const +{ + if (m_frame->selection()->isNone()) + return false; + + Node* startNode; + if (m_frame->selection()->isRange()) { + startNode = m_frame->selection()->selection().start().downstream().node(); + Node* endNode = m_frame->selection()->selection().end().upstream().node(); + if (enclosingBlock(startNode) != enclosingBlock(endNode)) + return false; + } else + startNode = m_frame->selection()->selection().visibleStart().deepEquivalent().node(); + + RenderObject* renderer = startNode->renderer(); + while (renderer && !renderer->isRenderBlock()) + renderer = renderer->parent(); + + if (!renderer) + return false; + + RenderStyle* style = renderer->style(); + if (style->direction() == RTL) + return true; + + return toRenderBlock(renderer)->containsNonZeroBidiLevel(); +} + +TriState Editor::selectionUnorderedListState() const +{ + if (m_frame->selection()->isCaret()) { + if (enclosingNodeWithTag(m_frame->selection()->selection().start(), ulTag)) + return TrueTriState; + } else if (m_frame->selection()->isRange()) { + Node* startNode = enclosingNodeWithTag(m_frame->selection()->selection().start(), ulTag); + Node* endNode = enclosingNodeWithTag(m_frame->selection()->selection().end(), ulTag); + if (startNode && endNode && startNode == endNode) + return TrueTriState; + } + + return FalseTriState; +} + +TriState Editor::selectionOrderedListState() const +{ + if (m_frame->selection()->isCaret()) { + if (enclosingNodeWithTag(m_frame->selection()->selection().start(), olTag)) + return TrueTriState; + } else if (m_frame->selection()->isRange()) { + Node* startNode = enclosingNodeWithTag(m_frame->selection()->selection().start(), olTag); + Node* endNode = enclosingNodeWithTag(m_frame->selection()->selection().end(), olTag); + if (startNode && endNode && startNode == endNode) + return TrueTriState; + } + + return FalseTriState; +} + +PassRefPtr Editor::insertOrderedList() +{ + if (!canEditRichly()) + return 0; + + RefPtr newList = InsertListCommand::insertList(m_frame->document(), InsertListCommand::OrderedList); + revealSelectionAfterEditingOperation(); + return newList; +} + +PassRefPtr Editor::insertUnorderedList() +{ + if (!canEditRichly()) + return 0; + + RefPtr newList = InsertListCommand::insertList(m_frame->document(), InsertListCommand::UnorderedList); + revealSelectionAfterEditingOperation(); + return newList; +} + +bool Editor::canIncreaseSelectionListLevel() +{ + return canEditRichly() && IncreaseSelectionListLevelCommand::canIncreaseSelectionListLevel(m_frame->document()); +} + +bool Editor::canDecreaseSelectionListLevel() +{ + return canEditRichly() && DecreaseSelectionListLevelCommand::canDecreaseSelectionListLevel(m_frame->document()); +} + +PassRefPtr Editor::increaseSelectionListLevel() +{ + if (!canEditRichly() || m_frame->selection()->isNone()) + return 0; + + RefPtr newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevel(m_frame->document()); + revealSelectionAfterEditingOperation(); + return newList; +} + +PassRefPtr Editor::increaseSelectionListLevelOrdered() +{ + if (!canEditRichly() || m_frame->selection()->isNone()) + return 0; + + RefPtr newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelOrdered(m_frame->document()); + revealSelectionAfterEditingOperation(); + return newList.release(); +} + +PassRefPtr Editor::increaseSelectionListLevelUnordered() +{ + if (!canEditRichly() || m_frame->selection()->isNone()) + return 0; + + RefPtr newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelUnordered(m_frame->document()); + revealSelectionAfterEditingOperation(); + return newList.release(); +} + +void Editor::decreaseSelectionListLevel() +{ + if (!canEditRichly() || m_frame->selection()->isNone()) + return; + + DecreaseSelectionListLevelCommand::decreaseSelectionListLevel(m_frame->document()); + revealSelectionAfterEditingOperation(); +} + +void Editor::removeFormattingAndStyle() +{ + applyCommand(RemoveFormatCommand::create(m_frame->document())); +} + +void Editor::clearLastEditCommand() +{ + m_lastEditCommand.clear(); +} + +// Returns whether caller should continue with "the default processing", which is the same as +// the event handler NOT setting the return value to false +bool Editor::dispatchCPPEvent(const AtomicString &eventType, ClipboardAccessPolicy policy) +{ + Node* target = m_frame->selection()->start().element(); + if (!target) + target = m_frame->document()->body(); + if (!target) + return true; + target = target->shadowAncestorNode(); + + RefPtr clipboard = newGeneralClipboard(policy); + + ExceptionCode ec = 0; + RefPtr evt = ClipboardEvent::create(eventType, true, true, clipboard); + target->dispatchEvent(evt, ec); + bool noDefaultProcessing = evt->defaultPrevented(); + + // invalidate clipboard here for security + clipboard->setAccessPolicy(ClipboardNumb); + + return !noDefaultProcessing; +} + +void Editor::applyStyle(CSSStyleDeclaration* style, EditAction editingAction) +{ + switch (m_frame->selection()->selectionType()) { + case VisibleSelection::NoSelection: + // do nothing + break; + case VisibleSelection::CaretSelection: + m_frame->computeAndSetTypingStyle(style, editingAction); + break; + case VisibleSelection::RangeSelection: + if (style) + applyCommand(ApplyStyleCommand::create(m_frame->document(), style, editingAction)); + break; + } +} + +bool Editor::shouldApplyStyle(CSSStyleDeclaration* style, Range* range) +{ + return client()->shouldApplyStyle(style, range); +} + +void Editor::applyParagraphStyle(CSSStyleDeclaration* style, EditAction editingAction) +{ + switch (m_frame->selection()->selectionType()) { + case VisibleSelection::NoSelection: + // do nothing + break; + case VisibleSelection::CaretSelection: + case VisibleSelection::RangeSelection: + if (style) + applyCommand(ApplyStyleCommand::create(m_frame->document(), style, editingAction, ApplyStyleCommand::ForceBlockProperties)); + break; + } +} + +void Editor::applyStyleToSelection(CSSStyleDeclaration* style, EditAction editingAction) +{ + if (!style || !style->length() || !canEditRichly()) + return; + + if (client() && client()->shouldApplyStyle(style, m_frame->selection()->toNormalizedRange().get())) + applyStyle(style, editingAction); +} + +void Editor::applyParagraphStyleToSelection(CSSStyleDeclaration* style, EditAction editingAction) +{ + if (!style || !style->length() || !canEditRichly()) + return; + + if (client() && client()->shouldApplyStyle(style, m_frame->selection()->toNormalizedRange().get())) + applyParagraphStyle(style, editingAction); +} + +bool Editor::clientIsEditable() const +{ + return client() && client()->isEditable(); +} + +// CSS properties that only has a visual difference when applied to text. +static const int textOnlyProperties[] = { + CSSPropertyTextDecoration, + CSSPropertyWebkitTextDecorationsInEffect, + CSSPropertyFontStyle, + CSSPropertyFontWeight, + CSSPropertyColor, +}; + +static TriState triStateOfStyleInComputedStyle(CSSStyleDeclaration* desiredStyle, CSSComputedStyleDeclaration* computedStyle, bool ignoreTextOnlyProperties = false) +{ + RefPtr diff = getPropertiesNotInComputedStyle(desiredStyle, computedStyle); + + if (ignoreTextOnlyProperties) + diff->removePropertiesInSet(textOnlyProperties, sizeof(textOnlyProperties) / sizeof(textOnlyProperties[0])); + + if (!diff->length()) + return TrueTriState; + if (diff->length() == desiredStyle->length()) + return FalseTriState; + return MixedTriState; +} + +bool Editor::selectionStartHasStyle(CSSStyleDeclaration* style) const +{ + Node* nodeToRemove; + RefPtr selectionStyle = m_frame->selectionComputedStyle(nodeToRemove); + if (!selectionStyle) + return false; + TriState state = triStateOfStyleInComputedStyle(style, selectionStyle.get()); + if (nodeToRemove) { + ExceptionCode ec = 0; + nodeToRemove->remove(ec); + ASSERT(!ec); + } + return state == TrueTriState; +} + +TriState Editor::selectionHasStyle(CSSStyleDeclaration* style) const +{ + TriState state = FalseTriState; + + if (!m_frame->selection()->isRange()) { + Node* nodeToRemove; + RefPtr selectionStyle = m_frame->selectionComputedStyle(nodeToRemove); + if (!selectionStyle) + return FalseTriState; + state = triStateOfStyleInComputedStyle(style, selectionStyle.get()); + if (nodeToRemove) { + ExceptionCode ec = 0; + nodeToRemove->remove(ec); + ASSERT(!ec); + } + } else { + for (Node* node = m_frame->selection()->start().node(); node; node = node->traverseNextNode()) { + RefPtr nodeStyle = computedStyle(node); + if (nodeStyle) { + TriState nodeState = triStateOfStyleInComputedStyle(style, nodeStyle.get(), !node->isTextNode()); + if (node == m_frame->selection()->start().node()) + state = nodeState; + else if (state != nodeState && node->isTextNode()) { + state = MixedTriState; + break; + } + } + if (node == m_frame->selection()->end().node()) + break; + } + } + + return state; +} +void Editor::indent() +{ + applyCommand(IndentOutdentCommand::create(m_frame->document(), IndentOutdentCommand::Indent)); +} + +void Editor::outdent() +{ + applyCommand(IndentOutdentCommand::create(m_frame->document(), IndentOutdentCommand::Outdent)); +} + +static void dispatchEditableContentChangedEvents(const EditCommand& command) +{ + Element* startRoot = command.startingRootEditableElement(); + Element* endRoot = command.endingRootEditableElement(); + ExceptionCode ec; + if (startRoot) + startRoot->dispatchEvent(Event::create(eventNames().webkitEditableContentChangedEvent, false, false), ec); + if (endRoot && endRoot != startRoot) + endRoot->dispatchEvent(Event::create(eventNames().webkitEditableContentChangedEvent, false, false), ec); +} + +void Editor::appliedEditing(PassRefPtr cmd) +{ + m_frame->document()->updateLayout(); + + dispatchEditableContentChangedEvents(*cmd); + + VisibleSelection newSelection(cmd->endingSelection()); + // Don't clear the typing style with this selection change. We do those things elsewhere if necessary. + changeSelectionAfterCommand(newSelection, false, false); + + if (!cmd->preservesTypingStyle()) + m_frame->setTypingStyle(0); + + // Command will be equal to last edit command only in the case of typing + if (m_lastEditCommand.get() == cmd) + ASSERT(cmd->isTypingCommand()); + else { + // Only register a new undo command if the command passed in is + // different from the last command + m_lastEditCommand = cmd; + if (client()) + client()->registerCommandForUndo(m_lastEditCommand); + } + respondToChangedContents(newSelection); +} + +void Editor::unappliedEditing(PassRefPtr cmd) +{ + m_frame->document()->updateLayout(); + + dispatchEditableContentChangedEvents(*cmd); + + VisibleSelection newSelection(cmd->startingSelection()); + changeSelectionAfterCommand(newSelection, true, true); + + m_lastEditCommand = 0; + if (client()) + client()->registerCommandForRedo(cmd); + respondToChangedContents(newSelection); +} + +void Editor::reappliedEditing(PassRefPtr cmd) +{ + m_frame->document()->updateLayout(); + + dispatchEditableContentChangedEvents(*cmd); + + VisibleSelection newSelection(cmd->endingSelection()); + changeSelectionAfterCommand(newSelection, true, true); + + m_lastEditCommand = 0; + if (client()) + client()->registerCommandForUndo(cmd); + respondToChangedContents(newSelection); +} + +Editor::Editor(Frame* frame) + : m_frame(frame) + , m_deleteButtonController(new DeleteButtonController(frame)) + , m_ignoreCompositionSelectionChange(false) + , m_shouldStartNewKillRingSequence(false) + // This is off by default, since most editors want this behavior (this matches IE but not FF). + , m_shouldStyleWithCSS(false) + , m_killRing(new KillRing) +{ +} + +Editor::~Editor() +{ +} + +void Editor::clear() +{ + m_compositionNode = 0; + m_customCompositionUnderlines.clear(); + m_shouldStyleWithCSS = false; +} + +bool Editor::insertText(const String& text, Event* triggeringEvent) +{ + return m_frame->eventHandler()->handleTextInputEvent(text, triggeringEvent); +} + +bool Editor::insertTextWithoutSendingTextEvent(const String& text, bool selectInsertedText, Event* triggeringEvent) +{ + if (text.isEmpty()) + return false; + + VisibleSelection selection = selectionForCommand(triggeringEvent); + if (!selection.isContentEditable()) + return false; + RefPtr range = selection.toNormalizedRange(); + + if (!shouldInsertText(text, range.get(), EditorInsertActionTyped)) + return true; + + // Get the selection to use for the event that triggered this insertText. + // If the event handler changed the selection, we may want to use a different selection + // that is contained in the event target. + selection = selectionForCommand(triggeringEvent); + if (selection.isContentEditable()) { + if (Node* selectionStart = selection.start().node()) { + RefPtr document = selectionStart->document(); + + // Insert the text + TypingCommand::insertText(document.get(), text, selection, selectInsertedText); + + // Reveal the current selection + if (Frame* editedFrame = document->frame()) + if (Page* page = editedFrame->page()) + page->focusController()->focusedOrMainFrame()->revealSelection(ScrollAlignment::alignToEdgeIfNeeded); + } + } + + return true; +} + +bool Editor::insertLineBreak() +{ + if (!canEdit()) + return false; + + if (!shouldInsertText("\n", m_frame->selection()->toNormalizedRange().get(), EditorInsertActionTyped)) + return true; + + TypingCommand::insertLineBreak(m_frame->document()); + revealSelectionAfterEditingOperation(); + return true; +} + +bool Editor::insertParagraphSeparator() +{ + if (!canEdit()) + return false; + + if (!canEditRichly()) + return insertLineBreak(); + + if (!shouldInsertText("\n", m_frame->selection()->toNormalizedRange().get(), EditorInsertActionTyped)) + return true; + + TypingCommand::insertParagraphSeparator(m_frame->document()); + revealSelectionAfterEditingOperation(); + return true; +} + +void Editor::cut() +{ + if (tryDHTMLCut()) + return; // DHTML did the whole operation + if (!canCut()) { + systemBeep(); + return; + } + RefPtr selection = selectedRange(); + if (shouldDeleteRange(selection.get())) { + if (isNodeInTextFormControl(m_frame->selection()->start().node())) + Pasteboard::generalPasteboard()->writePlainText(m_frame->selectedText()); + else + Pasteboard::generalPasteboard()->writeSelection(selection.get(), canSmartCopyOrDelete(), m_frame); + didWriteSelectionToPasteboard(); + deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); + } +} + +void Editor::copy() +{ + if (tryDHTMLCopy()) + return; // DHTML did the whole operation + if (!canCopy()) { + systemBeep(); + return; + } + + if (isNodeInTextFormControl(m_frame->selection()->start().node())) + Pasteboard::generalPasteboard()->writePlainText(m_frame->selectedText()); + else { + Document* document = m_frame->document(); + if (HTMLImageElement* imageElement = imageElementFromImageDocument(document)) + Pasteboard::generalPasteboard()->writeImage(imageElement, document->url(), document->title()); + else + Pasteboard::generalPasteboard()->writeSelection(selectedRange().get(), canSmartCopyOrDelete(), m_frame); + } + + didWriteSelectionToPasteboard(); +} + +#if !PLATFORM(MAC) + +void Editor::paste() +{ + ASSERT(m_frame->document()); + if (tryDHTMLPaste()) + return; // DHTML did the whole operation + if (!canPaste()) + return; + DocLoader* loader = m_frame->document()->docLoader(); + loader->setAllowStaleResources(true); + if (m_frame->selection()->isContentRichlyEditable()) + pasteWithPasteboard(Pasteboard::generalPasteboard(), true); + else + pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard()); + loader->setAllowStaleResources(false); +} + +#endif + +void Editor::pasteAsPlainText() +{ + if (tryDHTMLPaste()) + return; + if (!canPaste()) + return; + pasteAsPlainTextWithPasteboard(Pasteboard::generalPasteboard()); +} + +void Editor::performDelete() +{ + if (!canDelete()) { + systemBeep(); + return; + } + + addToKillRing(selectedRange().get(), false); + deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); + + // clear the "start new kill ring sequence" setting, because it was set to true + // when the selection was updated by deleting the range + setStartNewKillRingSequence(false); +} + +void Editor::copyURL(const KURL& url, const String& title) +{ + Pasteboard::generalPasteboard()->writeURL(url, title, m_frame); +} + +void Editor::copyImage(const HitTestResult& result) +{ + KURL url = result.absoluteLinkURL(); + if (url.isEmpty()) + url = result.absoluteImageURL(); + + Pasteboard::generalPasteboard()->writeImage(result.innerNonSharedNode(), url, result.altDisplayString()); +} + +bool Editor::isContinuousSpellCheckingEnabled() +{ + return client() && client()->isContinuousSpellCheckingEnabled(); +} + +void Editor::toggleContinuousSpellChecking() +{ + if (client()) + client()->toggleContinuousSpellChecking(); +} + +bool Editor::isGrammarCheckingEnabled() +{ + return client() && client()->isGrammarCheckingEnabled(); +} + +void Editor::toggleGrammarChecking() +{ + if (client()) + client()->toggleGrammarChecking(); +} + +int Editor::spellCheckerDocumentTag() +{ + return client() ? client()->spellCheckerDocumentTag() : 0; +} + +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + +void Editor::uppercaseWord() +{ + if (client()) + client()->uppercaseWord(); +} + +void Editor::lowercaseWord() +{ + if (client()) + client()->lowercaseWord(); +} + +void Editor::capitalizeWord() +{ + if (client()) + client()->capitalizeWord(); +} + +void Editor::showSubstitutionsPanel() +{ + if (!client()) { + LOG_ERROR("No NSSpellChecker"); + return; + } + + if (client()->substitutionsPanelIsShowing()) { + client()->showSubstitutionsPanel(false); + return; + } + client()->showSubstitutionsPanel(true); +} + +bool Editor::substitutionsPanelIsShowing() +{ + if (!client()) + return false; + return client()->substitutionsPanelIsShowing(); +} + +void Editor::toggleSmartInsertDelete() +{ + if (client()) + client()->toggleSmartInsertDelete(); +} + +bool Editor::isAutomaticQuoteSubstitutionEnabled() +{ + return client() && client()->isAutomaticQuoteSubstitutionEnabled(); +} + +void Editor::toggleAutomaticQuoteSubstitution() +{ + if (client()) + client()->toggleAutomaticQuoteSubstitution(); +} + +bool Editor::isAutomaticLinkDetectionEnabled() +{ + return client() && client()->isAutomaticLinkDetectionEnabled(); +} + +void Editor::toggleAutomaticLinkDetection() +{ + if (client()) + client()->toggleAutomaticLinkDetection(); +} + +bool Editor::isAutomaticDashSubstitutionEnabled() +{ + return client() && client()->isAutomaticDashSubstitutionEnabled(); +} + +void Editor::toggleAutomaticDashSubstitution() +{ + if (client()) + client()->toggleAutomaticDashSubstitution(); +} + +bool Editor::isAutomaticTextReplacementEnabled() +{ + return client() && client()->isAutomaticTextReplacementEnabled(); +} + +void Editor::toggleAutomaticTextReplacement() +{ + if (client()) + client()->toggleAutomaticTextReplacement(); +} + +bool Editor::isAutomaticSpellingCorrectionEnabled() +{ + return client() && client()->isAutomaticSpellingCorrectionEnabled(); +} + +void Editor::toggleAutomaticSpellingCorrection() +{ + if (client()) + client()->toggleAutomaticSpellingCorrection(); +} + +#endif + +bool Editor::shouldEndEditing(Range* range) +{ + return client() && client()->shouldEndEditing(range); +} + +bool Editor::shouldBeginEditing(Range* range) +{ + return client() && client()->shouldBeginEditing(range); +} + +void Editor::clearUndoRedoOperations() +{ + if (client()) + client()->clearUndoRedoOperations(); +} + +bool Editor::canUndo() +{ + return client() && client()->canUndo(); +} + +void Editor::undo() +{ + if (client()) + client()->undo(); +} + +bool Editor::canRedo() +{ + return client() && client()->canRedo(); +} + +void Editor::redo() +{ + if (client()) + client()->redo(); +} + +void Editor::didBeginEditing() +{ + if (client()) + client()->didBeginEditing(); +} + +void Editor::didEndEditing() +{ + if (client()) + client()->didEndEditing(); +} + +void Editor::didWriteSelectionToPasteboard() +{ + if (client()) + client()->didWriteSelectionToPasteboard(); +} + +void Editor::toggleBold() +{ + command("ToggleBold").execute(); +} + +void Editor::toggleUnderline() +{ + command("ToggleUnderline").execute(); +} + +void Editor::setBaseWritingDirection(WritingDirection direction) +{ + Node* focusedNode = frame()->document()->focusedNode(); + if (focusedNode && (focusedNode->hasTagName(textareaTag) + || (focusedNode->hasTagName(inputTag) && (static_cast(focusedNode)->inputType() == HTMLInputElement::TEXT + || static_cast(focusedNode)->inputType() == HTMLInputElement::SEARCH)))) { + if (direction == NaturalWritingDirection) + return; + static_cast(focusedNode)->setAttribute(dirAttr, direction == LeftToRightWritingDirection ? "ltr" : "rtl"); + frame()->document()->updateStyleIfNeeded(); + return; + } + + RefPtr style = CSSMutableStyleDeclaration::create(); + style->setProperty(CSSPropertyDirection, direction == LeftToRightWritingDirection ? "ltr" : direction == RightToLeftWritingDirection ? "rtl" : "inherit", false); + applyParagraphStyleToSelection(style.get(), EditActionSetWritingDirection); +} + +void Editor::selectComposition() +{ + RefPtr range = compositionRange(); + if (!range) + return; + + // The composition can start inside a composed character sequence, so we have to override checks. + // See + VisibleSelection selection; + selection.setWithoutValidation(range->startPosition(), range->endPosition()); + m_frame->selection()->setSelection(selection, false, false); +} + +void Editor::confirmComposition() +{ + if (!m_compositionNode) + return; + confirmComposition(m_compositionNode->data().substring(m_compositionStart, m_compositionEnd - m_compositionStart), false); +} + +void Editor::confirmCompositionWithoutDisturbingSelection() +{ + if (!m_compositionNode) + return; + confirmComposition(m_compositionNode->data().substring(m_compositionStart, m_compositionEnd - m_compositionStart), true); +} + +void Editor::confirmComposition(const String& text) +{ + confirmComposition(text, false); +} + +void Editor::confirmComposition(const String& text, bool preserveSelection) +{ + setIgnoreCompositionSelectionChange(true); + + VisibleSelection oldSelection = m_frame->selection()->selection(); + + selectComposition(); + + if (m_frame->selection()->isNone()) { + setIgnoreCompositionSelectionChange(false); + return; + } + + // Dispatch a compositionend event to the focused node. + // We should send this event before sending a TextEvent as written in Section 6.2.2 and 6.2.3 of + // the DOM Event specification. + Node* target = m_frame->document()->focusedNode(); + if (target) { + RefPtr event = CompositionEvent::create(eventNames().compositionendEvent, m_frame->domWindow(), text); + ExceptionCode ec = 0; + target->dispatchEvent(event, ec); + } + + // If text is empty, then delete the old composition here. If text is non-empty, InsertTextCommand::input + // will delete the old composition with an optimized replace operation. + if (text.isEmpty()) + TypingCommand::deleteSelection(m_frame->document(), false); + + m_compositionNode = 0; + m_customCompositionUnderlines.clear(); + + insertText(text, 0); + + if (preserveSelection) { + m_frame->selection()->setSelection(oldSelection, false, false); + // An open typing command that disagrees about current selection would cause issues with typing later on. + TypingCommand::closeTyping(m_lastEditCommand.get()); + } + + setIgnoreCompositionSelectionChange(false); +} + +void Editor::setComposition(const String& text, const Vector& underlines, unsigned selectionStart, unsigned selectionEnd) +{ + setIgnoreCompositionSelectionChange(true); + + selectComposition(); + + if (m_frame->selection()->isNone()) { + setIgnoreCompositionSelectionChange(false); + return; + } + + Node* target = m_frame->document()->focusedNode(); + if (target) { + // Dispatch an appropriate composition event to the focused node. + // We check the composition status and choose an appropriate composition event since this + // function is used for three purposes: + // 1. Starting a new composition. + // Send a compositionstart event when this function creates a new composition node, i.e. + // m_compositionNode == 0 && !text.isEmpty(). + // 2. Updating the existing composition node. + // Send a compositionupdate event when this function updates the existing composition + // node, i.e. m_compositionNode != 0 && !text.isEmpty(). + // 3. Canceling the ongoing composition. + // Send a compositionend event when function deletes the existing composition node, i.e. + // m_compositionNode != 0 && test.isEmpty(). + RefPtr event; + if (!m_compositionNode) { + // We should send a compositionstart event only when the given text is not empty because this + // function doesn't create a composition node when the text is empty. + if (!text.isEmpty()) + event = CompositionEvent::create(eventNames().compositionstartEvent, m_frame->domWindow(), text); + } else { + if (!text.isEmpty()) + event = CompositionEvent::create(eventNames().compositionupdateEvent, m_frame->domWindow(), text); + else + event = CompositionEvent::create(eventNames().compositionendEvent, m_frame->domWindow(), text); + } + ExceptionCode ec = 0; + if (event.get()) + target->dispatchEvent(event, ec); + } + + // If text is empty, then delete the old composition here. If text is non-empty, InsertTextCommand::input + // will delete the old composition with an optimized replace operation. + if (text.isEmpty()) + TypingCommand::deleteSelection(m_frame->document(), false); + + m_compositionNode = 0; + m_customCompositionUnderlines.clear(); + + if (!text.isEmpty()) { + TypingCommand::insertText(m_frame->document(), text, true, true); + + Node* baseNode = m_frame->selection()->base().node(); + unsigned baseOffset = m_frame->selection()->base().deprecatedEditingOffset(); + Node* extentNode = m_frame->selection()->extent().node(); + unsigned extentOffset = m_frame->selection()->extent().deprecatedEditingOffset(); + + if (baseNode && baseNode == extentNode && baseNode->isTextNode() && baseOffset + text.length() == extentOffset) { + m_compositionNode = static_cast(baseNode); + m_compositionStart = baseOffset; + m_compositionEnd = extentOffset; + m_customCompositionUnderlines = underlines; + size_t numUnderlines = m_customCompositionUnderlines.size(); + for (size_t i = 0; i < numUnderlines; ++i) { + m_customCompositionUnderlines[i].startOffset += baseOffset; + m_customCompositionUnderlines[i].endOffset += baseOffset; + } + if (baseNode->renderer()) + baseNode->renderer()->repaint(); + + unsigned start = min(baseOffset + selectionStart, extentOffset); + unsigned end = min(max(start, baseOffset + selectionEnd), extentOffset); + RefPtr selectedRange = Range::create(baseNode->document(), baseNode, start, baseNode, end); + m_frame->selection()->setSelectedRange(selectedRange.get(), DOWNSTREAM, false); + } + } + + setIgnoreCompositionSelectionChange(false); +} + +void Editor::ignoreSpelling() +{ + if (!client()) + return; + + RefPtr selectedRange = frame()->selection()->toNormalizedRange(); + if (selectedRange) + frame()->document()->removeMarkers(selectedRange.get(), DocumentMarker::Spelling); + + String text = frame()->selectedText(); + ASSERT(text.length()); + client()->ignoreWordInSpellDocument(text); +} + +void Editor::learnSpelling() +{ + if (!client()) + return; + + // FIXME: We don't call this on the Mac, and it should remove misspelling markers around the + // learned word, see . + + String text = frame()->selectedText(); + ASSERT(text.length()); + client()->learnWord(text); +} + +static String findFirstMisspellingInRange(EditorClient* client, Range* searchRange, int& firstMisspellingOffset, bool markAll, RefPtr& firstMisspellingRange) +{ + ASSERT_ARG(client, client); + ASSERT_ARG(searchRange, searchRange); + + WordAwareIterator it(searchRange); + firstMisspellingOffset = 0; + + String firstMisspelling; + int currentChunkOffset = 0; + + while (!it.atEnd()) { + const UChar* chars = it.characters(); + int len = it.length(); + + // Skip some work for one-space-char hunks + if (!(len == 1 && chars[0] == ' ')) { + + int misspellingLocation = -1; + int misspellingLength = 0; + client->checkSpellingOfString(chars, len, &misspellingLocation, &misspellingLength); + + // 5490627 shows that there was some code path here where the String constructor below crashes. + // We don't know exactly what combination of bad input caused this, so we're making this much + // more robust against bad input on release builds. + ASSERT(misspellingLength >= 0); + ASSERT(misspellingLocation >= -1); + ASSERT(!misspellingLength || misspellingLocation >= 0); + ASSERT(misspellingLocation < len); + ASSERT(misspellingLength <= len); + ASSERT(misspellingLocation + misspellingLength <= len); + + if (misspellingLocation >= 0 && misspellingLength > 0 && misspellingLocation < len && misspellingLength <= len && misspellingLocation + misspellingLength <= len) { + + // Compute range of misspelled word + RefPtr misspellingRange = TextIterator::subrange(searchRange, currentChunkOffset + misspellingLocation, misspellingLength); + + // Remember first-encountered misspelling and its offset. + if (!firstMisspelling) { + firstMisspellingOffset = currentChunkOffset + misspellingLocation; + firstMisspelling = String(chars + misspellingLocation, misspellingLength); + firstMisspellingRange = misspellingRange; + } + + // Store marker for misspelled word. + ExceptionCode ec = 0; + misspellingRange->startContainer(ec)->document()->addMarker(misspellingRange.get(), DocumentMarker::Spelling); + ASSERT(!ec); + + // Bail out if we're marking only the first misspelling, and not all instances. + if (!markAll) + break; + } + } + + currentChunkOffset += len; + it.advance(); + } + + return firstMisspelling; +} + +#ifndef BUILDING_ON_TIGER + +static PassRefPtr paragraphAlignedRangeForRange(Range* arbitraryRange, int& offsetIntoParagraphAlignedRange, String& paragraphString) +{ + ASSERT_ARG(arbitraryRange, arbitraryRange); + + ExceptionCode ec = 0; + + // Expand range to paragraph boundaries + RefPtr paragraphRange = arbitraryRange->cloneRange(ec); + setStart(paragraphRange.get(), startOfParagraph(arbitraryRange->startPosition())); + setEnd(paragraphRange.get(), endOfParagraph(arbitraryRange->endPosition())); + + // Compute offset from start of expanded range to start of original range + RefPtr offsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), arbitraryRange->startPosition()); + offsetIntoParagraphAlignedRange = TextIterator::rangeLength(offsetAsRange.get()); + + // Fill in out parameter with string representing entire paragraph range. + // Someday we might have a caller that doesn't use this, but for now all callers do. + paragraphString = plainText(paragraphRange.get()); + + return paragraphRange; +} + +static int findFirstGrammarDetailInRange(const Vector& grammarDetails, int badGrammarPhraseLocation, int /*badGrammarPhraseLength*/, Range *searchRange, int startOffset, int endOffset, bool markAll) +{ + // Found some bad grammar. Find the earliest detail range that starts in our search range (if any). + // Optionally add a DocumentMarker for each detail in the range. + int earliestDetailLocationSoFar = -1; + int earliestDetailIndex = -1; + for (unsigned i = 0; i < grammarDetails.size(); i++) { + const GrammarDetail* detail = &grammarDetails[i]; + ASSERT(detail->length > 0 && detail->location >= 0); + + int detailStartOffsetInParagraph = badGrammarPhraseLocation + detail->location; + + // Skip this detail if it starts before the original search range + if (detailStartOffsetInParagraph < startOffset) + continue; + + // Skip this detail if it starts after the original search range + if (detailStartOffsetInParagraph >= endOffset) + continue; + + if (markAll) { + RefPtr badGrammarRange = TextIterator::subrange(searchRange, badGrammarPhraseLocation - startOffset + detail->location, detail->length); + ExceptionCode ec = 0; + badGrammarRange->startContainer(ec)->document()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription); + ASSERT(!ec); + } + + // Remember this detail only if it's earlier than our current candidate (the details aren't in a guaranteed order) + if (earliestDetailIndex < 0 || earliestDetailLocationSoFar > detail->location) { + earliestDetailIndex = i; + earliestDetailLocationSoFar = detail->location; + } + } + + return earliestDetailIndex; +} + +static String findFirstBadGrammarInRange(EditorClient* client, Range* searchRange, GrammarDetail& outGrammarDetail, int& outGrammarPhraseOffset, bool markAll) +{ + ASSERT_ARG(client, client); + ASSERT_ARG(searchRange, searchRange); + + // Initialize out parameters; these will be updated if we find something to return. + outGrammarDetail.location = -1; + outGrammarDetail.length = 0; + outGrammarDetail.guesses.clear(); + outGrammarDetail.userDescription = ""; + outGrammarPhraseOffset = 0; + + String firstBadGrammarPhrase; + + // Expand the search range to encompass entire paragraphs, since grammar checking needs that much context. + // Determine the character offset from the start of the paragraph to the start of the original search range, + // since we will want to ignore results in this area. + int searchRangeStartOffset; + String paragraphString; + RefPtr paragraphRange = paragraphAlignedRangeForRange(searchRange, searchRangeStartOffset, paragraphString); + + // Determine the character offset from the start of the paragraph to the end of the original search range, + // since we will want to ignore results in this area also. + int searchRangeEndOffset = searchRangeStartOffset + TextIterator::rangeLength(searchRange); + + // Start checking from beginning of paragraph, but skip past results that occur before the start of the original search range. + int startOffset = 0; + while (startOffset < searchRangeEndOffset) { + Vector grammarDetails; + int badGrammarPhraseLocation = -1; + int badGrammarPhraseLength = 0; + client->checkGrammarOfString(paragraphString.characters() + startOffset, paragraphString.length() - startOffset, grammarDetails, &badGrammarPhraseLocation, &badGrammarPhraseLength); + + if (!badGrammarPhraseLength) { + ASSERT(badGrammarPhraseLocation == -1); + return String(); + } + + ASSERT(badGrammarPhraseLocation >= 0); + badGrammarPhraseLocation += startOffset; + + + // Found some bad grammar. Find the earliest detail range that starts in our search range (if any). + int badGrammarIndex = findFirstGrammarDetailInRange(grammarDetails, badGrammarPhraseLocation, badGrammarPhraseLength, searchRange, searchRangeStartOffset, searchRangeEndOffset, markAll); + if (badGrammarIndex >= 0) { + ASSERT(static_cast(badGrammarIndex) < grammarDetails.size()); + outGrammarDetail = grammarDetails[badGrammarIndex]; + } + + // If we found a detail in range, then we have found the first bad phrase (unless we found one earlier but + // kept going so we could mark all instances). + if (badGrammarIndex >= 0 && firstBadGrammarPhrase.isEmpty()) { + outGrammarPhraseOffset = badGrammarPhraseLocation - searchRangeStartOffset; + firstBadGrammarPhrase = paragraphString.substring(badGrammarPhraseLocation, badGrammarPhraseLength); + + // Found one. We're done now, unless we're marking each instance. + if (!markAll) + break; + } + + // These results were all between the start of the paragraph and the start of the search range; look + // beyond this phrase. + startOffset = badGrammarPhraseLocation + badGrammarPhraseLength; + } + + return firstBadGrammarPhrase; +} + +#endif /* not BUILDING_ON_TIGER */ + +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + +static String findFirstMisspellingOrBadGrammarInRange(EditorClient* client, Range* searchRange, bool checkGrammar, bool& outIsSpelling, int& outFirstFoundOffset, GrammarDetail& outGrammarDetail) +{ + ASSERT_ARG(client, client); + ASSERT_ARG(searchRange, searchRange); + + String firstFoundItem; + String misspelledWord; + String badGrammarPhrase; + ExceptionCode ec = 0; + + // Initialize out parameters; these will be updated if we find something to return. + outIsSpelling = true; + outFirstFoundOffset = 0; + outGrammarDetail.location = -1; + outGrammarDetail.length = 0; + outGrammarDetail.guesses.clear(); + outGrammarDetail.userDescription = ""; + + // Expand the search range to encompass entire paragraphs, since text checking needs that much context. + // Determine the character offset from the start of the paragraph to the start of the original search range, + // since we will want to ignore results in this area. + RefPtr paragraphRange = searchRange->cloneRange(ec); + setStart(paragraphRange.get(), startOfParagraph(searchRange->startPosition())); + int totalRangeLength = TextIterator::rangeLength(paragraphRange.get()); + setEnd(paragraphRange.get(), endOfParagraph(searchRange->startPosition())); + + RefPtr offsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), searchRange->startPosition()); + int searchRangeStartOffset = TextIterator::rangeLength(offsetAsRange.get()); + int totalLengthProcessed = 0; + + bool firstIteration = true; + bool lastIteration = false; + while (totalLengthProcessed < totalRangeLength) { + // Iterate through the search range by paragraphs, checking each one for spelling and grammar. + int currentLength = TextIterator::rangeLength(paragraphRange.get()); + int currentStartOffset = firstIteration ? searchRangeStartOffset : 0; + int currentEndOffset = currentLength; + if (inSameParagraph(paragraphRange->startPosition(), searchRange->endPosition())) { + // Determine the character offset from the end of the original search range to the end of the paragraph, + // since we will want to ignore results in this area. + RefPtr endOffsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), searchRange->endPosition()); + currentEndOffset = TextIterator::rangeLength(endOffsetAsRange.get()); + lastIteration = true; + } + if (currentStartOffset < currentEndOffset) { + String paragraphString = plainText(paragraphRange.get()); + if (paragraphString.length() > 0) { + bool foundGrammar = false; + int spellingLocation = 0; + int grammarPhraseLocation = 0; + int grammarDetailLocation = 0; + unsigned grammarDetailIndex = 0; + + Vector results; + uint64_t checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling; + client->checkTextOfParagraph(paragraphString.characters(), paragraphString.length(), checkingTypes, results); + + for (unsigned i = 0; i < results.size(); i++) { + const TextCheckingResult* result = &results[i]; + if (result->type == TextCheckingTypeSpelling && result->location >= currentStartOffset && result->location + result->length <= currentEndOffset) { + ASSERT(result->length > 0 && result->location >= 0); + spellingLocation = result->location; + misspelledWord = paragraphString.substring(result->location, result->length); + ASSERT(misspelledWord.length()); + break; + } + if (checkGrammar && result->type == TextCheckingTypeGrammar && result->location < currentEndOffset && result->location + result->length > currentStartOffset) { + ASSERT(result->length > 0 && result->location >= 0); + // We can't stop after the first grammar result, since there might still be a spelling result after + // it begins but before the first detail in it, but we can stop if we find a second grammar result. + if (foundGrammar) + break; + for (unsigned j = 0; j < result->details.size(); j++) { + const GrammarDetail* detail = &result->details[j]; + ASSERT(detail->length > 0 && detail->location >= 0); + if (result->location + detail->location >= currentStartOffset && result->location + detail->location + detail->length <= currentEndOffset && (!foundGrammar || result->location + detail->location < grammarDetailLocation)) { + grammarDetailIndex = j; + grammarDetailLocation = result->location + detail->location; + foundGrammar = true; + } + } + if (foundGrammar) { + grammarPhraseLocation = result->location; + outGrammarDetail = result->details[grammarDetailIndex]; + badGrammarPhrase = paragraphString.substring(result->location, result->length); + ASSERT(badGrammarPhrase.length()); + } + } + } + + if (!misspelledWord.isEmpty() && (!checkGrammar || badGrammarPhrase.isEmpty() || spellingLocation <= grammarDetailLocation)) { + int spellingOffset = spellingLocation - currentStartOffset; + if (!firstIteration) { + RefPtr paragraphOffsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), searchRange->startPosition(), paragraphRange->startPosition()); + spellingOffset += TextIterator::rangeLength(paragraphOffsetAsRange.get()); + } + outIsSpelling = true; + outFirstFoundOffset = spellingOffset; + firstFoundItem = misspelledWord; + break; + } + if (checkGrammar && !badGrammarPhrase.isEmpty()) { + int grammarPhraseOffset = grammarPhraseLocation - currentStartOffset; + if (!firstIteration) { + RefPtr paragraphOffsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), searchRange->startPosition(), paragraphRange->startPosition()); + grammarPhraseOffset += TextIterator::rangeLength(paragraphOffsetAsRange.get()); + } + outIsSpelling = false; + outFirstFoundOffset = grammarPhraseOffset; + firstFoundItem = badGrammarPhrase; + break; + } + } + } + if (lastIteration || totalLengthProcessed + currentLength >= totalRangeLength) + break; + VisiblePosition newParagraphStart = startOfNextParagraph(paragraphRange->endPosition()); + setStart(paragraphRange.get(), newParagraphStart); + setEnd(paragraphRange.get(), endOfParagraph(newParagraphStart)); + firstIteration = false; + totalLengthProcessed += currentLength; + } + return firstFoundItem; +} + +#endif + +void Editor::advanceToNextMisspelling(bool startBeforeSelection) +{ + ExceptionCode ec = 0; + + // The basic approach is to search in two phases - from the selection end to the end of the doc, and + // then we wrap and search from the doc start to (approximately) where we started. + + // Start at the end of the selection, search to edge of document. Starting at the selection end makes + // repeated "check spelling" commands work. + VisibleSelection selection(frame()->selection()->selection()); + RefPtr spellingSearchRange(rangeOfContents(frame()->document())); + bool startedWithSelection = false; + if (selection.start().node()) { + startedWithSelection = true; + if (startBeforeSelection) { + VisiblePosition start(selection.visibleStart()); + // We match AppKit's rule: Start 1 character before the selection. + VisiblePosition oneBeforeStart = start.previous(); + setStart(spellingSearchRange.get(), oneBeforeStart.isNotNull() ? oneBeforeStart : start); + } else + setStart(spellingSearchRange.get(), selection.visibleEnd()); + } + + Position position = spellingSearchRange->startPosition(); + if (!isEditablePosition(position)) { + // This shouldn't happen in very often because the Spelling menu items aren't enabled unless the + // selection is editable. + // This can happen in Mail for a mix of non-editable and editable content (like Stationary), + // when spell checking the whole document before sending the message. + // In that case the document might not be editable, but there are editable pockets that need to be spell checked. + + position = firstEditablePositionAfterPositionInRoot(position, frame()->document()->documentElement()).deepEquivalent(); + if (position.isNull()) + return; + + Position rangeCompliantPosition = rangeCompliantEquivalent(position); + spellingSearchRange->setStart(rangeCompliantPosition.node(), rangeCompliantPosition.deprecatedEditingOffset(), ec); + startedWithSelection = false; // won't need to wrap + } + + // topNode defines the whole range we want to operate on + Node* topNode = highestEditableRoot(position); + // FIXME: lastOffsetForEditing() is wrong here if editingIgnoresContent(highestEditableRoot()) returns true (e.g. a ) + spellingSearchRange->setEnd(topNode, lastOffsetForEditing(topNode), ec); + + // If spellingSearchRange starts in the middle of a word, advance to the next word so we start checking + // at a word boundary. Going back by one char and then forward by a word does the trick. + if (startedWithSelection) { + VisiblePosition oneBeforeStart = startVisiblePosition(spellingSearchRange.get(), DOWNSTREAM).previous(); + if (oneBeforeStart.isNotNull()) + setStart(spellingSearchRange.get(), endOfWord(oneBeforeStart)); + // else we were already at the start of the editable node + } + + if (spellingSearchRange->collapsed(ec)) + return; // nothing to search in + + // Get the spell checker if it is available + if (!client()) + return; + + // We go to the end of our first range instead of the start of it, just to be sure + // we don't get foiled by any word boundary problems at the start. It means we might + // do a tiny bit more searching. + Node* searchEndNodeAfterWrap = spellingSearchRange->endContainer(ec); + int searchEndOffsetAfterWrap = spellingSearchRange->endOffset(ec); + + int misspellingOffset = 0; +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + RefPtr grammarSearchRange = spellingSearchRange->cloneRange(ec); + String misspelledWord; + String badGrammarPhrase; + int grammarPhraseOffset = 0; + bool isSpelling = true; + int foundOffset = 0; + GrammarDetail grammarDetail; + String foundItem = findFirstMisspellingOrBadGrammarInRange(client(), spellingSearchRange.get(), isGrammarCheckingEnabled(), isSpelling, foundOffset, grammarDetail); + if (isSpelling) { + misspelledWord = foundItem; + misspellingOffset = foundOffset; + } else { + badGrammarPhrase = foundItem; + grammarPhraseOffset = foundOffset; + } +#else + RefPtr firstMisspellingRange; + String misspelledWord = findFirstMisspellingInRange(client(), spellingSearchRange.get(), misspellingOffset, false, firstMisspellingRange); + String badGrammarPhrase; + +#ifndef BUILDING_ON_TIGER + int grammarPhraseOffset = 0; + GrammarDetail grammarDetail; + + // Search for bad grammar that occurs prior to the next misspelled word (if any) + RefPtr grammarSearchRange = spellingSearchRange->cloneRange(ec); + if (!misspelledWord.isEmpty()) { + // Stop looking at start of next misspelled word + CharacterIterator chars(grammarSearchRange.get()); + chars.advance(misspellingOffset); + grammarSearchRange->setEnd(chars.range()->startContainer(ec), chars.range()->startOffset(ec), ec); + } + + if (isGrammarCheckingEnabled()) + badGrammarPhrase = findFirstBadGrammarInRange(client(), grammarSearchRange.get(), grammarDetail, grammarPhraseOffset, false); +#endif +#endif + + // If we found neither bad grammar nor a misspelled word, wrap and try again (but don't bother if we started at the beginning of the + // block rather than at a selection). + if (startedWithSelection && !misspelledWord && !badGrammarPhrase) { + spellingSearchRange->setStart(topNode, 0, ec); + // going until the end of the very first chunk we tested is far enough + spellingSearchRange->setEnd(searchEndNodeAfterWrap, searchEndOffsetAfterWrap, ec); + +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + grammarSearchRange = spellingSearchRange->cloneRange(ec); + foundItem = findFirstMisspellingOrBadGrammarInRange(client(), spellingSearchRange.get(), isGrammarCheckingEnabled(), isSpelling, foundOffset, grammarDetail); + if (isSpelling) { + misspelledWord = foundItem; + misspellingOffset = foundOffset; + } else { + badGrammarPhrase = foundItem; + grammarPhraseOffset = foundOffset; + } +#else + misspelledWord = findFirstMisspellingInRange(client(), spellingSearchRange.get(), misspellingOffset, false, firstMisspellingRange); + +#ifndef BUILDING_ON_TIGER + grammarSearchRange = spellingSearchRange->cloneRange(ec); + if (!misspelledWord.isEmpty()) { + // Stop looking at start of next misspelled word + CharacterIterator chars(grammarSearchRange.get()); + chars.advance(misspellingOffset); + grammarSearchRange->setEnd(chars.range()->startContainer(ec), chars.range()->startOffset(ec), ec); + } + if (isGrammarCheckingEnabled()) + badGrammarPhrase = findFirstBadGrammarInRange(client(), grammarSearchRange.get(), grammarDetail, grammarPhraseOffset, false); +#endif +#endif + } + + if (!badGrammarPhrase.isEmpty()) { +#ifdef BUILDING_ON_TIGER + ASSERT_NOT_REACHED(); +#else + // We found bad grammar. Since we only searched for bad grammar up to the first misspelled word, the bad grammar + // takes precedence and we ignore any potential misspelled word. Select the grammar detail, update the spelling + // panel, and store a marker so we draw the green squiggle later. + + ASSERT(badGrammarPhrase.length() > 0); + ASSERT(grammarDetail.location != -1 && grammarDetail.length > 0); + + // FIXME 4859190: This gets confused with doubled punctuation at the end of a paragraph + RefPtr badGrammarRange = TextIterator::subrange(grammarSearchRange.get(), grammarPhraseOffset + grammarDetail.location, grammarDetail.length); + frame()->selection()->setSelection(VisibleSelection(badGrammarRange.get(), SEL_DEFAULT_AFFINITY)); + frame()->revealSelection(); + + client()->updateSpellingUIWithGrammarString(badGrammarPhrase, grammarDetail); + frame()->document()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, grammarDetail.userDescription); +#endif + } else if (!misspelledWord.isEmpty()) { + // We found a misspelling, but not any earlier bad grammar. Select the misspelling, update the spelling panel, and store + // a marker so we draw the red squiggle later. + + RefPtr misspellingRange = TextIterator::subrange(spellingSearchRange.get(), misspellingOffset, misspelledWord.length()); + frame()->selection()->setSelection(VisibleSelection(misspellingRange.get(), DOWNSTREAM)); + frame()->revealSelection(); + + client()->updateSpellingUIWithMisspelledWord(misspelledWord); + frame()->document()->addMarker(misspellingRange.get(), DocumentMarker::Spelling); + } +} + +bool Editor::isSelectionMisspelled() +{ + String selectedString = frame()->selectedText(); + int length = selectedString.length(); + if (!length) + return false; + + if (!client()) + return false; + + int misspellingLocation = -1; + int misspellingLength = 0; + client()->checkSpellingOfString(selectedString.characters(), length, &misspellingLocation, &misspellingLength); + + // The selection only counts as misspelled if the selected text is exactly one misspelled word + if (misspellingLength != length) + return false; + + // Update the spelling panel to be displaying this error (whether or not the spelling panel is on screen). + // This is necessary to make a subsequent call to [NSSpellChecker ignoreWord:inSpellDocumentWithTag:] work + // correctly; that call behaves differently based on whether the spelling panel is displaying a misspelling + // or a grammar error. + client()->updateSpellingUIWithMisspelledWord(selectedString); + + return true; +} + +#ifndef BUILDING_ON_TIGER +static bool isRangeUngrammatical(EditorClient* client, Range *range, Vector& guessesVector) +{ + if (!client) + return false; + + ExceptionCode ec; + if (!range || range->collapsed(ec)) + return false; + + // Returns true only if the passed range exactly corresponds to a bad grammar detail range. This is analogous + // to isSelectionMisspelled. It's not good enough for there to be some bad grammar somewhere in the range, + // or overlapping the range; the ranges must exactly match. + guessesVector.clear(); + int grammarPhraseOffset; + + GrammarDetail grammarDetail; + String badGrammarPhrase = findFirstBadGrammarInRange(client, range, grammarDetail, grammarPhraseOffset, false); + + // No bad grammar in these parts at all. + if (badGrammarPhrase.isEmpty()) + return false; + + // Bad grammar, but phrase (e.g. sentence) starts beyond start of range. + if (grammarPhraseOffset > 0) + return false; + + ASSERT(grammarDetail.location >= 0 && grammarDetail.length > 0); + + // Bad grammar, but start of detail (e.g. ungrammatical word) doesn't match start of range + if (grammarDetail.location + grammarPhraseOffset) + return false; + + // Bad grammar at start of range, but end of bad grammar is before or after end of range + if (grammarDetail.length != TextIterator::rangeLength(range)) + return false; + + // Update the spelling panel to be displaying this error (whether or not the spelling panel is on screen). + // This is necessary to make a subsequent call to [NSSpellChecker ignoreWord:inSpellDocumentWithTag:] work + // correctly; that call behaves differently based on whether the spelling panel is displaying a misspelling + // or a grammar error. + client->updateSpellingUIWithGrammarString(badGrammarPhrase, grammarDetail); + + return true; +} +#endif + +bool Editor::isSelectionUngrammatical() +{ +#ifdef BUILDING_ON_TIGER + return false; +#else + Vector ignoredGuesses; + return isRangeUngrammatical(client(), frame()->selection()->toNormalizedRange().get(), ignoredGuesses); +#endif +} + +Vector Editor::guessesForUngrammaticalSelection() +{ +#ifdef BUILDING_ON_TIGER + return Vector(); +#else + Vector guesses; + // Ignore the result of isRangeUngrammatical; we just want the guesses, whether or not there are any + isRangeUngrammatical(client(), frame()->selection()->toNormalizedRange().get(), guesses); + return guesses; +#endif +} + +Vector Editor::guessesForMisspelledSelection() +{ + String selectedString = frame()->selectedText(); + ASSERT(selectedString.length()); + + Vector guesses; + if (client()) + client()->getGuessesForWord(selectedString, guesses); + return guesses; +} + +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + +static Vector guessesForMisspelledOrUngrammaticalRange(EditorClient* client, Range *range, bool checkGrammar, bool& misspelled, bool& ungrammatical) +{ + Vector guesses; + ExceptionCode ec; + misspelled = false; + ungrammatical = false; + + if (!client || !range || range->collapsed(ec)) + return guesses; + + // Expand the range to encompass entire paragraphs, since text checking needs that much context. + int rangeStartOffset; + String paragraphString; + RefPtr paragraphRange = paragraphAlignedRangeForRange(range, rangeStartOffset, paragraphString); + int rangeLength = TextIterator::rangeLength(range); + if (!rangeLength || !paragraphString.length()) + return guesses; + + Vector results; + uint64_t checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling; + client->checkTextOfParagraph(paragraphString.characters(), paragraphString.length(), checkingTypes, results); + + for (unsigned i = 0; i < results.size(); i++) { + const TextCheckingResult* result = &results[i]; + if (result->type == TextCheckingTypeSpelling && result->location == rangeStartOffset && result->length == rangeLength) { + String misspelledWord = paragraphString.substring(rangeStartOffset, rangeLength); + ASSERT(misspelledWord.length()); + client->getGuessesForWord(misspelledWord, guesses); + client->updateSpellingUIWithMisspelledWord(misspelledWord); + misspelled = true; + return guesses; + } + } + + if (!checkGrammar) + return guesses; + + for (unsigned i = 0; i < results.size(); i++) { + const TextCheckingResult* result = &results[i]; + if (result->type == TextCheckingTypeGrammar && result->location <= rangeStartOffset && result->location + result->length >= rangeStartOffset + rangeLength) { + for (unsigned j = 0; j < result->details.size(); j++) { + const GrammarDetail* detail = &result->details[j]; + ASSERT(detail->length > 0 && detail->location >= 0); + if (result->location + detail->location == rangeStartOffset && detail->length == rangeLength) { + String badGrammarPhrase = paragraphString.substring(result->location, result->length); + ASSERT(badGrammarPhrase.length()); + for (unsigned k = 0; k < detail->guesses.size(); k++) + guesses.append(detail->guesses[k]); + client->updateSpellingUIWithGrammarString(badGrammarPhrase, *detail); + ungrammatical = true; + return guesses; + } + } + } + } + return guesses; +} + +#endif + +Vector Editor::guessesForMisspelledOrUngrammaticalSelection(bool& misspelled, bool& ungrammatical) +{ +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + return guessesForMisspelledOrUngrammaticalRange(client(), frame()->selection()->toNormalizedRange().get(), isGrammarCheckingEnabled(), misspelled, ungrammatical); +#else + misspelled = isSelectionMisspelled(); + if (misspelled) { + ungrammatical = false; + return guessesForMisspelledSelection(); + } + if (isGrammarCheckingEnabled() && isSelectionUngrammatical()) { + ungrammatical = true; + return guessesForUngrammaticalSelection(); + } + ungrammatical = false; + return Vector(); +#endif +} + +void Editor::showSpellingGuessPanel() +{ + if (!client()) { + LOG_ERROR("No NSSpellChecker"); + return; + } + +#ifndef BUILDING_ON_TIGER + // Post-Tiger, this menu item is a show/hide toggle, to match AppKit. Leave Tiger behavior alone + // to match rest of OS X. + if (client()->spellingUIIsShowing()) { + client()->showSpellingUI(false); + return; + } +#endif + + advanceToNextMisspelling(true); + client()->showSpellingUI(true); +} + +bool Editor::spellingPanelIsShowing() +{ + if (!client()) + return false; + return client()->spellingUIIsShowing(); +} + +void Editor::markMisspellingsAfterTypingToPosition(const VisiblePosition &p) +{ +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + bool markSpelling = isContinuousSpellCheckingEnabled(); + bool markGrammar = markSpelling && isGrammarCheckingEnabled(); + bool performTextCheckingReplacements = isAutomaticQuoteSubstitutionEnabled() + || isAutomaticLinkDetectionEnabled() + || isAutomaticDashSubstitutionEnabled() + || isAutomaticTextReplacementEnabled() + || (markSpelling && isAutomaticSpellingCorrectionEnabled()); + if (!markSpelling && !performTextCheckingReplacements) + return; + + VisibleSelection adjacentWords = VisibleSelection(startOfWord(p, LeftWordIfOnBoundary), endOfWord(p, RightWordIfOnBoundary)); + if (markGrammar) { + VisibleSelection selectedSentence = VisibleSelection(startOfSentence(p), endOfSentence(p)); + markAllMisspellingsAndBadGrammarInRanges(true, adjacentWords.toNormalizedRange().get(), true, selectedSentence.toNormalizedRange().get(), performTextCheckingReplacements); + } else { + markAllMisspellingsAndBadGrammarInRanges(markSpelling, adjacentWords.toNormalizedRange().get(), false, adjacentWords.toNormalizedRange().get(), performTextCheckingReplacements); + } +#else + if (!isContinuousSpellCheckingEnabled()) + return; + + // Check spelling of one word + RefPtr misspellingRange; + markMisspellings(VisibleSelection(startOfWord(p, LeftWordIfOnBoundary), endOfWord(p, RightWordIfOnBoundary)), misspellingRange); + + // Autocorrect the misspelled word. + if (!misspellingRange) + return; + + // Get the misspelled word. + const String misspelledWord = plainText(misspellingRange.get()); + String autocorrectedString = client()->getAutoCorrectSuggestionForMisspelledWord(misspelledWord); + + // If autocorrected word is non empty, replace the misspelled word by this word. + if (!autocorrectedString.isEmpty()) { + VisibleSelection newSelection(misspellingRange.get(), DOWNSTREAM); + if (newSelection != frame()->selection()->selection()) { + if (!frame()->shouldChangeSelection(newSelection)) + return; + frame()->selection()->setSelection(newSelection); + } + + if (!frame()->editor()->shouldInsertText(autocorrectedString, misspellingRange.get(), EditorInsertActionTyped)) + return; + frame()->editor()->replaceSelectionWithText(autocorrectedString, false, false); + + // Reset the charet one character further. + frame()->selection()->moveTo(frame()->selection()->end()); + frame()->selection()->modify(SelectionController::AlterationMove, SelectionController::DirectionForward, CharacterGranularity); + } + + if (!isGrammarCheckingEnabled()) + return; + + // Check grammar of entire sentence + markBadGrammar(VisibleSelection(startOfSentence(p), endOfSentence(p))); +#endif +} + +static void markAllMisspellingsInRange(EditorClient* client, Range* searchRange, RefPtr& firstMisspellingRange) +{ + // Use the "markAll" feature of findFirstMisspellingInRange. Ignore the return value and the "out parameter"; + // all we need to do is mark every instance. + int ignoredOffset; + findFirstMisspellingInRange(client, searchRange, ignoredOffset, true, firstMisspellingRange); +} + +#ifndef BUILDING_ON_TIGER +static void markAllBadGrammarInRange(EditorClient* client, Range* searchRange) +{ + // Use the "markAll" feature of findFirstBadGrammarInRange. Ignore the return value and "out parameters"; all we need to + // do is mark every instance. + GrammarDetail ignoredGrammarDetail; + int ignoredOffset; + findFirstBadGrammarInRange(client, searchRange, ignoredGrammarDetail, ignoredOffset, true); +} +#endif + +static void markMisspellingsOrBadGrammar(Editor* editor, const VisibleSelection& selection, bool checkSpelling, RefPtr& firstMisspellingRange) +{ + // This function is called with a selection already expanded to word boundaries. + // Might be nice to assert that here. + + // This function is used only for as-you-type checking, so if that's off we do nothing. Note that + // grammar checking can only be on if spell checking is also on. + if (!editor->isContinuousSpellCheckingEnabled()) + return; + + RefPtr searchRange(selection.toNormalizedRange()); + if (!searchRange) + return; + + // If we're not in an editable node, bail. + Node* editableNode = searchRange->startContainer(); + if (!editableNode || !editableNode->isContentEditable()) + return; + + if (!editor->spellCheckingEnabledInFocusedNode()) + return; + + // Get the spell checker if it is available + if (!editor->client()) + return; + + if (checkSpelling) + markAllMisspellingsInRange(editor->client(), searchRange.get(), firstMisspellingRange); + else { +#ifdef BUILDING_ON_TIGER + ASSERT_NOT_REACHED(); +#else + if (editor->isGrammarCheckingEnabled()) + markAllBadGrammarInRange(editor->client(), searchRange.get()); +#endif + } +} + +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(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& firstMisspellingRange) +{ + markMisspellingsOrBadGrammar(this, selection, true, firstMisspellingRange); +} + +void Editor::markBadGrammar(const VisibleSelection& selection) +{ +#ifndef BUILDING_ON_TIGER + RefPtr firstMisspellingRange; + markMisspellingsOrBadGrammar(this, selection, false, firstMisspellingRange); +#else + UNUSED_PARAM(selection); +#endif +} + +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + +static inline bool isAmbiguousBoundaryCharacter(UChar character) +{ + // These are characters that can behave as word boundaries, but can appear within words. + // If they are just typed, i.e. if they are immediately followed by a caret, we want to delay text checking until the next character has been typed. + // FIXME: this is required until 6853027 is fixed and text checking can do this for us. + return character == '\'' || character == rightSingleQuotationMark || character == hebrewPunctuationGershayim; +} + +void Editor::markAllMisspellingsAndBadGrammarInRanges(bool markSpelling, Range* spellingRange, bool markGrammar, Range* grammarRange, bool performTextCheckingReplacements) +{ + // This function is called with selections already expanded to word boundaries. + ExceptionCode ec = 0; + if (!client() || !spellingRange || (markGrammar && !grammarRange)) + return; + + // If we're not in an editable node, bail. + 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; + int grammarRangeStartOffset = 0; + int grammarRangeEndOffset = 0; + int offsetDueToReplacement = 0; + int paragraphLength = 0; + int selectionOffset = 0; + int ambiguousBoundaryOffset = -1; + bool selectionChanged = false; + bool restoreSelectionAfterChange = false; + bool adjustSelectionForParagraphBoundaries = false; + String paragraphString; + RefPtr paragraphRange; + + if (markGrammar) { + // The spelling range should be contained in the paragraph-aligned extension of the grammar range. + paragraphRange = paragraphAlignedRangeForRange(grammarRange, grammarRangeStartOffset, paragraphString); + RefPtr offsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), spellingRange->startPosition()); + spellingRangeStartOffset = TextIterator::rangeLength(offsetAsRange.get()); + grammarRangeEndOffset = grammarRangeStartOffset + TextIterator::rangeLength(grammarRange); + } else { + paragraphRange = paragraphAlignedRangeForRange(spellingRange, spellingRangeStartOffset, paragraphString); + } + spellingRangeEndOffset = spellingRangeStartOffset + TextIterator::rangeLength(spellingRange); + paragraphLength = paragraphString.length(); + if (paragraphLength <= 0 || (spellingRangeStartOffset >= spellingRangeEndOffset && (!markGrammar || grammarRangeStartOffset >= grammarRangeEndOffset))) + return; + + if (performTextCheckingReplacements) { + if (m_frame->selection()->selectionType() == VisibleSelection::CaretSelection) { + // Attempt to save the caret position so we can restore it later if needed + RefPtr offsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), paragraphRange->startPosition()); + Position caretPosition = m_frame->selection()->end(); + offsetAsRange->setEnd(caretPosition.containerNode(), caretPosition.computeOffsetInContainerNode(), ec); + if (!ec) { + selectionOffset = TextIterator::rangeLength(offsetAsRange.get()); + restoreSelectionAfterChange = true; + if (selectionOffset > 0 && (selectionOffset > paragraphLength || paragraphString[selectionOffset - 1] == newlineCharacter)) + adjustSelectionForParagraphBoundaries = true; + if (selectionOffset > 0 && selectionOffset <= paragraphLength && isAmbiguousBoundaryCharacter(paragraphString[selectionOffset - 1])) + ambiguousBoundaryOffset = selectionOffset - 1; + } + } + } + + Vector results; + uint64_t checkingTypes = 0; + if (markSpelling) + checkingTypes |= TextCheckingTypeSpelling; + if (markGrammar) + checkingTypes |= TextCheckingTypeGrammar; + if (performTextCheckingReplacements) { + if (isAutomaticLinkDetectionEnabled()) + checkingTypes |= TextCheckingTypeLink; + if (isAutomaticQuoteSubstitutionEnabled()) + checkingTypes |= TextCheckingTypeQuote; + if (isAutomaticDashSubstitutionEnabled()) + checkingTypes |= TextCheckingTypeDash; + if (isAutomaticTextReplacementEnabled()) + checkingTypes |= TextCheckingTypeReplacement; + if (markSpelling && isAutomaticSpellingCorrectionEnabled()) + checkingTypes |= TextCheckingTypeCorrection; + } + client()->checkTextOfParagraph(paragraphString.characters(), paragraphLength, checkingTypes, results); + + for (unsigned i = 0; i < results.size(); i++) { + const TextCheckingResult* result = &results[i]; + int resultLocation = result->location + offsetDueToReplacement; + int resultLength = result->length; + if (markSpelling && result->type == TextCheckingTypeSpelling && resultLocation >= spellingRangeStartOffset && resultLocation + resultLength <= spellingRangeEndOffset) { + ASSERT(resultLength > 0 && resultLocation >= 0); + RefPtr misspellingRange = TextIterator::subrange(spellingRange, resultLocation - spellingRangeStartOffset, resultLength); + misspellingRange->startContainer(ec)->document()->addMarker(misspellingRange.get(), DocumentMarker::Spelling); + } else if (markGrammar && result->type == TextCheckingTypeGrammar && resultLocation < grammarRangeEndOffset && resultLocation + resultLength > grammarRangeStartOffset) { + ASSERT(resultLength > 0 && resultLocation >= 0); + for (unsigned j = 0; j < result->details.size(); j++) { + const GrammarDetail* detail = &result->details[j]; + ASSERT(detail->length > 0 && detail->location >= 0); + if (resultLocation + detail->location >= grammarRangeStartOffset && resultLocation + detail->location + detail->length <= grammarRangeEndOffset) { + RefPtr badGrammarRange = TextIterator::subrange(grammarRange, resultLocation + detail->location - grammarRangeStartOffset, detail->length); + grammarRange->startContainer(ec)->document()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription); + } + } + } else if (performTextCheckingReplacements && resultLocation + resultLength <= spellingRangeEndOffset && resultLocation + resultLength >= spellingRangeStartOffset + && (result->type == TextCheckingTypeLink + || result->type == TextCheckingTypeQuote + || result->type == TextCheckingTypeDash + || result->type == TextCheckingTypeReplacement + || result->type == TextCheckingTypeCorrection)) { + // In this case the result range just has to touch the spelling range, so we can handle replacing non-word text such as punctuation. + ASSERT(resultLength > 0 && resultLocation >= 0); + int replacementLength = result->replacement.length(); + bool doReplacement = (replacementLength > 0); + RefPtr rangeToReplace = TextIterator::subrange(paragraphRange.get(), resultLocation, resultLength); + VisibleSelection selectionToReplace(rangeToReplace.get(), DOWNSTREAM); + + // avoid correcting text after an ambiguous boundary character has been typed + // FIXME: this is required until 6853027 is fixed and text checking can do this for us + if (ambiguousBoundaryOffset >= 0 && resultLocation + resultLength == ambiguousBoundaryOffset) + doReplacement = false; + + // adding links should be done only immediately after they are typed + if (result->type == TextCheckingTypeLink && selectionOffset > resultLocation + resultLength + 1) + doReplacement = false; + + // Don't correct spelling in an already-corrected word. + if (doReplacement && result->type == TextCheckingTypeCorrection) { + Node* node = rangeToReplace->startContainer(); + int startOffset = rangeToReplace->startOffset(); + int endOffset = startOffset + replacementLength; + Vector markers = node->document()->markersForNode(node); + size_t markerCount = markers.size(); + for (size_t i = 0; i < markerCount; ++i) { + const DocumentMarker& marker = markers[i]; + if (marker.type == DocumentMarker::Replacement && static_cast(marker.startOffset) < endOffset && static_cast(marker.endOffset) > startOffset) { + doReplacement = false; + break; + } + if (static_cast(marker.startOffset) >= endOffset) + break; + } + } + if (doReplacement && selectionToReplace != m_frame->selection()->selection()) { + if (m_frame->shouldChangeSelection(selectionToReplace)) { + m_frame->selection()->setSelection(selectionToReplace); + selectionChanged = true; + } else { + doReplacement = false; + } + } + if (doReplacement) { + if (result->type == TextCheckingTypeLink) { + restoreSelectionAfterChange = false; + if (canEditRichly()) + applyCommand(CreateLinkCommand::create(m_frame->document(), result->replacement)); + } else if (canEdit() && shouldInsertText(result->replacement, rangeToReplace.get(), EditorInsertActionTyped)) { + String replacedString; + if (result->type == TextCheckingTypeCorrection) + replacedString = plainText(rangeToReplace.get()); + replaceSelectionWithText(result->replacement, false, false); + spellingRangeEndOffset += replacementLength - resultLength; + offsetDueToReplacement += replacementLength - resultLength; + if (resultLocation < selectionOffset) + selectionOffset += replacementLength - resultLength; + if (result->type == TextCheckingTypeCorrection) { + // Add a marker so that corrections can easily be undone and won't be re-corrected. + RefPtr replacedRange = TextIterator::subrange(paragraphRange.get(), resultLocation, replacementLength); + replacedRange->startContainer()->document()->addMarker(replacedRange.get(), DocumentMarker::Replacement, replacedString); + } + } + } + } + } + + if (selectionChanged) { + // Restore the caret position if we have made any replacements + setEnd(paragraphRange.get(), endOfParagraph(startOfNextParagraph(paragraphRange->startPosition()))); + int newLength = TextIterator::rangeLength(paragraphRange.get()); + if (restoreSelectionAfterChange && selectionOffset >= 0 && selectionOffset <= newLength) { + RefPtr selectionRange = TextIterator::subrange(paragraphRange.get(), 0, selectionOffset); + m_frame->selection()->moveTo(selectionRange->endPosition(), DOWNSTREAM); + if (adjustSelectionForParagraphBoundaries) + m_frame->selection()->modify(SelectionController::AlterationMove, SelectionController::DirectionForward, CharacterGranularity); + } else { + // If this fails for any reason, the fallback is to go one position beyond the last replacement + m_frame->selection()->moveTo(m_frame->selection()->end()); + m_frame->selection()->modify(SelectionController::AlterationMove, SelectionController::DirectionForward, CharacterGranularity); + } + } +} + +void Editor::changeBackToReplacedString(const String& replacedString) +{ + if (replacedString.isEmpty()) + return; + + RefPtr selection = selectedRange(); + if (!shouldInsertText(replacedString, selection.get(), EditorInsertActionPasted)) + return; + + String paragraphString; + int selectionOffset; + RefPtr paragraphRange = paragraphAlignedRangeForRange(selection.get(), selectionOffset, paragraphString); + replaceSelectionWithText(replacedString, false, false); + RefPtr changedRange = TextIterator::subrange(paragraphRange.get(), selectionOffset, replacedString.length()); + changedRange->startContainer()->document()->addMarker(changedRange.get(), DocumentMarker::Replacement, String()); +} + +#endif + +void Editor::markMisspellingsAndBadGrammar(const VisibleSelection& spellingSelection, bool markGrammar, const VisibleSelection& grammarSelection) +{ +#if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) + if (!isContinuousSpellCheckingEnabled()) + return; + markAllMisspellingsAndBadGrammarInRanges(true, spellingSelection.toNormalizedRange().get(), markGrammar && isGrammarCheckingEnabled(), grammarSelection.toNormalizedRange().get(), false); +#else + RefPtr firstMisspellingRange; + markMisspellings(spellingSelection, firstMisspellingRange); + if (markGrammar) + markBadGrammar(grammarSelection); +#endif +} + +PassRefPtr Editor::rangeForPoint(const IntPoint& windowPoint) +{ + Document* document = m_frame->documentAtPoint(windowPoint); + if (!document) + return 0; + + Frame* frame = document->frame(); + ASSERT(frame); + FrameView* frameView = frame->view(); + if (!frameView) + return 0; + IntPoint framePoint = frameView->windowToContents(windowPoint); + VisibleSelection selection(frame->visiblePositionForPoint(framePoint)); + return avoidIntersectionWithNode(selection.toNormalizedRange().get(), m_deleteButtonController->containerElement()); +} + +void Editor::revealSelectionAfterEditingOperation() +{ + if (m_ignoreCompositionSelectionChange) + return; + + m_frame->revealSelection(ScrollAlignment::alignToEdgeIfNeeded); +} + +void Editor::setIgnoreCompositionSelectionChange(bool ignore) +{ + if (m_ignoreCompositionSelectionChange == ignore) + return; + + m_ignoreCompositionSelectionChange = ignore; + if (!ignore) + revealSelectionAfterEditingOperation(); +} + +PassRefPtr Editor::compositionRange() const +{ + if (!m_compositionNode) + return 0; + unsigned length = m_compositionNode->length(); + unsigned start = min(m_compositionStart, length); + unsigned end = min(max(start, m_compositionEnd), length); + if (start >= end) + return 0; + return Range::create(m_compositionNode->document(), m_compositionNode.get(), start, m_compositionNode.get(), end); +} + +bool Editor::getCompositionSelection(unsigned& selectionStart, unsigned& selectionEnd) const +{ + if (!m_compositionNode) + return false; + Position start = m_frame->selection()->start(); + if (start.node() != m_compositionNode) + return false; + Position end = m_frame->selection()->end(); + if (end.node() != m_compositionNode) + return false; + + if (static_cast(start.deprecatedEditingOffset()) < m_compositionStart) + return false; + if (static_cast(end.deprecatedEditingOffset()) > m_compositionEnd) + return false; + + selectionStart = start.deprecatedEditingOffset() - m_compositionStart; + selectionEnd = start.deprecatedEditingOffset() - m_compositionEnd; + return true; +} + +void Editor::transpose() +{ + if (!canEdit()) + return; + + VisibleSelection selection = m_frame->selection()->selection(); + if (!selection.isCaret()) + return; + + // Make a selection that goes back one character and forward two characters. + VisiblePosition caret = selection.visibleStart(); + VisiblePosition next = isEndOfParagraph(caret) ? caret : caret.next(); + VisiblePosition previous = next.previous(); + if (next == previous) + return; + previous = previous.previous(); + if (!inSameParagraph(next, previous)) + return; + RefPtr range = makeRange(previous, next); + if (!range) + return; + VisibleSelection newSelection(range.get(), DOWNSTREAM); + + // Transpose the two characters. + String text = plainText(range.get()); + if (text.length() != 2) + return; + String transposed = text.right(1) + text.left(1); + + // Select the two characters. + if (newSelection != m_frame->selection()->selection()) { + if (!m_frame->shouldChangeSelection(newSelection)) + return; + m_frame->selection()->setSelection(newSelection); + } + + // Insert the transposed characters. + if (!shouldInsertText(transposed, range.get(), EditorInsertActionTyped)) + return; + replaceSelectionWithText(transposed, false, false); +} + +void Editor::addToKillRing(Range* range, bool prepend) +{ + if (m_shouldStartNewKillRingSequence) + killRing()->startNewSequence(); + + String text = plainText(range); + if (prepend) + killRing()->prepend(text); + else + killRing()->append(text); + m_shouldStartNewKillRingSequence = false; +} + +bool Editor::insideVisibleArea(const IntPoint& point) const +{ + if (m_frame->excludeFromTextSearch()) + return false; + + // Right now, we only check the visibility of a point for disconnected frames. For all other + // frames, we assume visibility. + Frame* frame = m_frame->isDisconnected() ? m_frame : m_frame->tree()->top(true); + if (!frame->isDisconnected()) + return true; + + RenderPart* renderer = frame->ownerRenderer(); + if (!renderer) + return false; + + RenderBlock* container = renderer->containingBlock(); + if (!(container->style()->overflowX() == OHIDDEN || container->style()->overflowY() == OHIDDEN)) + return true; + + IntRect rectInPageCoords = container->overflowClipRect(0, 0); + IntRect rectInFrameCoords = IntRect(renderer->x() * -1, renderer->y() * -1, + rectInPageCoords.width(), rectInPageCoords.height()); + + return rectInFrameCoords.contains(point); +} + +bool Editor::insideVisibleArea(Range* range) const +{ + if (!range) + return true; + + if (m_frame->excludeFromTextSearch()) + return false; + + // Right now, we only check the visibility of a range for disconnected frames. For all other + // frames, we assume visibility. + Frame* frame = m_frame->isDisconnected() ? m_frame : m_frame->tree()->top(true); + if (!frame->isDisconnected()) + return true; + + RenderPart* renderer = frame->ownerRenderer(); + if (!renderer) + return false; + + RenderBlock* container = renderer->containingBlock(); + if (!(container->style()->overflowX() == OHIDDEN || container->style()->overflowY() == OHIDDEN)) + return true; + + IntRect rectInPageCoords = container->overflowClipRect(0, 0); + IntRect rectInFrameCoords = IntRect(renderer->x() * -1, renderer->y() * -1, + rectInPageCoords.width(), rectInPageCoords.height()); + IntRect resultRect = range->boundingBox(); + + return rectInFrameCoords.contains(resultRect); +} + +PassRefPtr Editor::firstVisibleRange(const String& target, bool caseFlag) +{ + RefPtr searchRange(rangeOfContents(m_frame->document())); + RefPtr resultRange = findPlainText(searchRange.get(), target, true, caseFlag); + ExceptionCode ec = 0; + + while (!insideVisibleArea(resultRange.get())) { + searchRange->setStartAfter(resultRange->endContainer(), ec); + if (searchRange->startContainer() == searchRange->endContainer()) + return Range::create(m_frame->document()); + resultRange = findPlainText(searchRange.get(), target, true, caseFlag); + } + + return resultRange; +} + +PassRefPtr Editor::lastVisibleRange(const String& target, bool caseFlag) +{ + RefPtr searchRange(rangeOfContents(m_frame->document())); + RefPtr resultRange = findPlainText(searchRange.get(), target, false, caseFlag); + ExceptionCode ec = 0; + + while (!insideVisibleArea(resultRange.get())) { + searchRange->setEndBefore(resultRange->startContainer(), ec); + if (searchRange->startContainer() == searchRange->endContainer()) + return Range::create(m_frame->document()); + resultRange = findPlainText(searchRange.get(), target, false, caseFlag); + } + + return resultRange; +} + +PassRefPtr Editor::nextVisibleRange(Range* currentRange, const String& target, bool forward, bool caseFlag, bool wrapFlag) +{ + if (m_frame->excludeFromTextSearch()) + return Range::create(m_frame->document()); + + RefPtr resultRange = currentRange; + RefPtr searchRange(rangeOfContents(m_frame->document())); + ExceptionCode ec = 0; + + for ( ; !insideVisibleArea(resultRange.get()); resultRange = findPlainText(searchRange.get(), target, forward, caseFlag)) { + if (resultRange->collapsed(ec)) { + if (!resultRange->startContainer()->isInShadowTree()) + break; + searchRange = rangeOfContents(m_frame->document()); + if (forward) + searchRange->setStartAfter(resultRange->startContainer()->shadowAncestorNode(), ec); + else + searchRange->setEndBefore(resultRange->startContainer()->shadowAncestorNode(), ec); + continue; + } + + if (forward) + searchRange->setStartAfter(resultRange->endContainer(), ec); + else + searchRange->setEndBefore(resultRange->startContainer(), ec); + + Node* shadowTreeRoot = searchRange->shadowTreeRootNode(); + if (searchRange->collapsed(ec) && shadowTreeRoot) { + if (forward) + searchRange->setEnd(shadowTreeRoot, shadowTreeRoot->childNodeCount(), ec); + else + searchRange->setStartBefore(shadowTreeRoot, ec); + } + + if (searchRange->startContainer()->isDocumentNode() && searchRange->endContainer()->isDocumentNode()) + break; + } + + if (insideVisibleArea(resultRange.get())) + return resultRange; + + if (!wrapFlag) + return Range::create(m_frame->document()); + + if (forward) + return firstVisibleRange(target, caseFlag); + + return lastVisibleRange(target, caseFlag); +} + +void Editor::changeSelectionAfterCommand(const VisibleSelection& newSelection, bool closeTyping, bool clearTypingStyle) +{ + // If there is no selection change, don't bother sending shouldChangeSelection, but still call setSelection, + // because there is work that it must do in this situation. + // The old selection can be invalid here and calling shouldChangeSelection can produce some strange calls. + // See Some shouldChangeSelectedDOMRange contain Ranges for selections that are no longer valid + bool selectionDidNotChangeDOMPosition = newSelection == m_frame->selection()->selection(); + if (selectionDidNotChangeDOMPosition || m_frame->shouldChangeSelection(newSelection)) + m_frame->selection()->setSelection(newSelection, closeTyping, clearTypingStyle); + + // Some editing operations change the selection visually without affecting its position within the DOM. + // For example when you press return in the following (the caret is marked by ^): + //
^Hello
+ // WebCore inserts

*before* the current block, which correctly moves the paragraph down but which doesn't + // change the caret's DOM position (["hello", 0]). In these situations the above SelectionController::setSelection call + // does not call EditorClient::respondToChangedSelection(), which, on the Mac, sends selection change notifications and + // starts a new kill ring sequence, but we want to do these things (matches AppKit). + if (selectionDidNotChangeDOMPosition) + client()->respondToChangedSelection(); +} + +} // namespace WebCore