/* * 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 "CharacterNames.h" #include "CreateLinkCommand.h" #include "CSSComputedStyleDeclaration.h" #include "CSSMutableStyleDeclaration.h" #include "CSSProperty.h" #include "CSSPropertyNames.h" #include "CSSValueKeywords.h" #include "ClipboardEvent.h" #include "DeleteButtonController.h" #include "DeleteSelectionCommand.h" #include "DocLoader.h" #include "DocumentFragment.h" #include "EditorClient.h" #include "EventHandler.h" #include "EventNames.h" #include "FocusController.h" #include "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 "ModifySelectionListLevel.h" #include "Page.h" #include "Pasteboard.h" #include "RemoveFormatCommand.h" #include "RenderBlock.h" #include "RenderPart.h" #include "ReplaceSelectionCommand.h" #include "Sound.h" #include "Text.h" #include "TextIterator.h" #include "TypingCommand.h" #include "htmlediting.h" #include "markup.h" #include "visible_units.h" #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; } 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::FORWARD: case SelectionController::RIGHT: TypingCommand::forwardDeleteKeyPressed(m_frame->document(), canSmartCopyOrDelete(), granularity, killRing); break; case SelectionController::BACKWARD: case SelectionController::LEFT: 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(), "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 == 0); } 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() == 0 || !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() == 0 || !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; else 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 == 0); } 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 == 0); } } 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) { 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) { 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, cmd.get()); 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) { dispatchEditableContentChangedEvents(*cmd); VisibleSelection newSelection(cmd->startingSelection()); changeSelectionAfterCommand(newSelection, true, true, cmd.get()); m_lastEditCommand = 0; if (client()) client()->registerCommandForRedo(cmd); respondToChangedContents(newSelection); } void Editor::reappliedEditing(PassRefPtr cmd) { dispatchEditableContentChangedEvents(*cmd); VisibleSelection newSelection(cmd->endingSelection()); changeSelectionAfterCommand(newSelection, true, true, cmd.get()); 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) { } 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())) { Pasteboard::generalPasteboard()->writeSelection(selection.get(), canSmartCopyOrDelete(), m_frame); didWriteSelectionToPasteboard(); deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); } } void Editor::copy() { if (tryDHTMLCopy()) return; // DHTML did the whole operation if (!canCopy()) { systemBeep(); return; } Document* document = m_frame->document(); if (HTMLImageElement* imageElement = imageElementFromImageDocument(document)) Pasteboard::generalPasteboard()->writeImage(imageElement, document->url(), document->title()); else Pasteboard::generalPasteboard()->writeSelection(selectedRange().get(), canSmartCopyOrDelete(), m_frame); didWriteSelectionToPasteboard(); } #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 (!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; } // 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); 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; } // 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() != 0); client()->ignoreWordInSpellDocument(text); } void Editor::learnSpelling() { if (!client()) return; // FIXME: We don't call this on the Mac, and it should remove misppelling markers around the // learned word, see . String text = frame()->selectedText(); ASSERT(text.length() != 0); 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 == 0 || misspellingLocation >= 0); ASSERT(misspellingLocation < len); ASSERT(misspellingLength <= len); ASSERT(misspellingLocation + misspellingLength <= len); if (misspellingLocation >= 0 && misspellingLength > 0 && misspellingLocation < len && misspellingLength <= len && misspellingLocation + misspellingLength <= len) { // 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 == 0); // 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 == 0); } // Remember this detail only if it's earlier than our current candidate (the details aren't in a guaranteed order) if (earliestDetailIndex < 0 || earliestDetailLocationSoFar > detail->location) { earliestDetailIndex = i; earliestDetailLocationSoFar = detail->location; } } return earliestDetailIndex; } static String findFirstBadGrammarInRange(EditorClient* client, Range* searchRange, GrammarDetail& outGrammarDetail, int& outGrammarPhraseOffset, bool markAll) { ASSERT_ARG(client, client); ASSERT_ARG(searchRange, searchRange); // Initialize out parameters; these will be updated if we find something to return. outGrammarDetail.location = -1; outGrammarDetail.length = 0; outGrammarDetail.guesses.clear(); outGrammarDetail.userDescription = ""; outGrammarPhraseOffset = 0; String firstBadGrammarPhrase; // Expand the search range to encompass entire paragraphs, since grammar checking needs that much context. // Determine the character offset from the start of the paragraph to the start of the original search range, // since we will want to ignore results in this area. int searchRangeStartOffset; String paragraphString; RefPtr 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 == 0) { ASSERT(badGrammarPhraseLocation == -1); return String(); } ASSERT(badGrammarPhraseLocation >= 0); badGrammarPhraseLocation += startOffset; // Found some bad grammar. Find the earliest detail range that starts in our search range (if any). int badGrammarIndex = findFirstGrammarDetailInRange(grammarDetails, badGrammarPhraseLocation, badGrammarPhraseLength, searchRange, searchRangeStartOffset, searchRangeEndOffset, markAll); if (badGrammarIndex >= 0) { ASSERT(static_cast(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() != 0); break; } else 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() != 0); } } } 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; } else 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; setStart(paragraphRange.get(), startOfNextParagraph(paragraphRange->endPosition())); setEnd(paragraphRange.get(), endOfParagraph(paragraphRange->startPosition())); 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 == 0) return false; if (!client()) return false; int misspellingLocation = -1; int misspellingLength = 0; client()->checkSpellingOfString(selectedString.characters(), length, &misspellingLocation, &misspellingLength); // The selection only counts as misspelled if the selected text is exactly one misspelled word if (misspellingLength != length) return false; // Update the spelling panel to be displaying this error (whether or not the spelling panel is on screen). // This is necessary to make a subsequent call to [NSSpellChecker ignoreWord:inSpellDocumentWithTag:] work // correctly; that call behaves differently based on whether the spelling panel is displaying a misspelling // or a grammar error. client()->updateSpellingUIWithMisspelledWord(selectedString); return true; } #ifndef BUILDING_ON_TIGER static bool isRangeUngrammatical(EditorClient* client, Range *range, Vector& guessesVector) { if (!client) return false; ExceptionCode ec; if (!range || range->collapsed(ec)) return false; // Returns true only if the passed range exactly corresponds to a bad grammar detail range. This is analogous // to isSelectionMisspelled. It's not good enough for there to be some bad grammar somewhere in the range, // or overlapping the range; the ranges must exactly match. guessesVector.clear(); int grammarPhraseOffset; GrammarDetail grammarDetail; String badGrammarPhrase = findFirstBadGrammarInRange(client, range, grammarDetail, grammarPhraseOffset, false); // No bad grammar in these parts at all. if (badGrammarPhrase.isEmpty()) return false; // Bad grammar, but phrase (e.g. sentence) starts beyond start of range. if (grammarPhraseOffset > 0) return false; ASSERT(grammarDetail.location >= 0 && grammarDetail.length > 0); // Bad grammar, but start of detail (e.g. ungrammatical word) doesn't match start of range if (grammarDetail.location + grammarPhraseOffset != 0) return false; // Bad grammar at start of range, but end of bad grammar is before or after end of range if (grammarDetail.length != TextIterator::rangeLength(range)) return false; // Update the spelling panel to be displaying this error (whether or not the spelling panel is on screen). // This is necessary to make a subsequent call to [NSSpellChecker ignoreWord:inSpellDocumentWithTag:] work // correctly; that call behaves differently based on whether the spelling panel is displaying a misspelling // or a grammar error. client->updateSpellingUIWithGrammarString(badGrammarPhrase, grammarDetail); return true; } #endif bool Editor::isSelectionUngrammatical() { #ifdef BUILDING_ON_TIGER return false; #else Vector 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() != 0); 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 == 0 || paragraphString.length() == 0) 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() != 0); 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() != 0); 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 == 0) 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::MOVE, SelectionController::FORWARD, 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::MOVE, SelectionController::FORWARD, 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::MOVE, SelectionController::FORWARD, 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) startNewKillRingSequence(); String text = m_frame->displayStringModifiedByEncoding(plainText(range)); if (prepend) prependToKillRing(text); else appendToKillRing(text); m_shouldStartNewKillRingSequence = false; } #if !PLATFORM(MAC) void Editor::appendToKillRing(const String&) { } void Editor::prependToKillRing(const String&) { } String Editor::yankFromKillRing() { return String(); } void Editor::startNewKillRingSequence() { } void Editor::setKillRingToYankedState() { } #endif 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(); 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(); 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, EditCommand* cmd) { // 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 kinds of deletes and line break insertions change the selection's position within the document without // changing 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 && cmd->isTypingCommand()) client()->respondToChangedSelection(); } } // namespace WebCore