| /* |
| * Copyright (C) 2006, 2007, 2008, 2011 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 "core/editing/spellcheck/SpellChecker.h" |
| |
| #include "core/HTMLNames.h" |
| #include "core/InputTypeNames.h" |
| #include "core/dom/Document.h" |
| #include "core/dom/Element.h" |
| #include "core/dom/ElementTraversal.h" |
| #include "core/dom/NodeTraversal.h" |
| #include "core/editing/EditingUtilities.h" |
| #include "core/editing/Editor.h" |
| #include "core/editing/EphemeralRange.h" |
| #include "core/editing/VisibleUnits.h" |
| #include "core/editing/commands/CompositeEditCommand.h" |
| #include "core/editing/commands/ReplaceSelectionCommand.h" |
| #include "core/editing/commands/TypingCommand.h" |
| #include "core/editing/iterators/CharacterIterator.h" |
| #include "core/editing/markers/DocumentMarkerController.h" |
| #include "core/editing/spellcheck/SpellCheckRequester.h" |
| #include "core/editing/spellcheck/TextCheckingParagraph.h" |
| #include "core/frame/LocalFrame.h" |
| #include "core/frame/Settings.h" |
| #include "core/html/HTMLInputElement.h" |
| #include "core/layout/LayoutTextControl.h" |
| #include "core/loader/EmptyClients.h" |
| #include "core/page/Page.h" |
| #include "core/page/SpellCheckerClient.h" |
| #include "platform/text/TextBreakIterator.h" |
| #include "platform/text/TextCheckerClient.h" |
| |
| namespace blink { |
| |
| using namespace HTMLNames; |
| |
| namespace { |
| |
| bool isSelectionInTextField(const VisibleSelection& selection) { |
| HTMLTextFormControlElement* textControl = |
| enclosingTextFormControl(selection.start()); |
| return isHTMLInputElement(textControl) && |
| toHTMLInputElement(textControl)->isTextField(); |
| } |
| |
| bool isSelectionInTextArea(const VisibleSelection& selection) { |
| HTMLTextFormControlElement* textControl = |
| enclosingTextFormControl(selection.start()); |
| return isHTMLTextAreaElement(textControl); |
| } |
| |
| bool isSelectionInTextFormControl(const VisibleSelection& selection) { |
| return !!enclosingTextFormControl(selection.start()); |
| } |
| |
| static bool isSpellCheckingEnabledFor(const VisibleSelection& selection) { |
| if (selection.isNone()) |
| return false; |
| // TODO(tkent): The following password type check should be done in |
| // HTMLElement::spellcheck(). crbug.com/371567 |
| if (HTMLTextFormControlElement* textControl = |
| enclosingTextFormControl(selection.start())) { |
| if (isHTMLInputElement(textControl) && |
| toHTMLInputElement(textControl)->type() == InputTypeNames::password) |
| return false; |
| } |
| if (HTMLElement* element = Traversal<HTMLElement>::firstAncestorOrSelf( |
| *selection.start().anchorNode())) { |
| if (element->isSpellCheckingEnabled()) |
| return true; |
| } |
| return false; |
| } |
| |
| static EphemeralRange expandEndToSentenceBoundary(const EphemeralRange& range) { |
| DCHECK(range.isNotNull()); |
| const VisiblePosition& visibleEnd = |
| createVisiblePosition(range.endPosition()); |
| DCHECK(visibleEnd.isNotNull()); |
| const Position& sentenceEnd = endOfSentence(visibleEnd).deepEquivalent(); |
| // TODO(xiaochengh): |sentenceEnd < range.endPosition()| is possible, |
| // which would trigger a DCHECK in EphemeralRange's constructor if we return |
| // it directly. However, this shouldn't happen and needs to be fixed. |
| return EphemeralRange( |
| range.startPosition(), |
| sentenceEnd.isNotNull() && sentenceEnd > range.endPosition() |
| ? sentenceEnd |
| : range.endPosition()); |
| } |
| |
| static EphemeralRange expandRangeToSentenceBoundary( |
| const EphemeralRange& range) { |
| DCHECK(range.isNotNull()); |
| const VisiblePosition& visibleStart = |
| createVisiblePosition(range.startPosition()); |
| DCHECK(visibleStart.isNotNull()); |
| const Position& sentenceStart = |
| startOfSentence(visibleStart).deepEquivalent(); |
| // TODO(xiaochengh): |sentenceStart > range.startPosition()| is possible, |
| // which would trigger a DCHECK in EphemeralRange's constructor if we return |
| // it directly. However, this shouldn't happen and needs to be fixed. |
| return expandEndToSentenceBoundary(EphemeralRange( |
| sentenceStart.isNotNull() && sentenceStart < range.startPosition() |
| ? sentenceStart |
| : range.startPosition(), |
| range.endPosition())); |
| } |
| |
| } // namespace |
| |
| SpellChecker* SpellChecker::create(LocalFrame& frame) { |
| return new SpellChecker(frame); |
| } |
| |
| static SpellCheckerClient& emptySpellCheckerClient() { |
| DEFINE_STATIC_LOCAL(EmptySpellCheckerClient, client, ()); |
| return client; |
| } |
| |
| SpellCheckerClient& SpellChecker::spellCheckerClient() const { |
| if (Page* page = frame().page()) |
| return page->spellCheckerClient(); |
| return emptySpellCheckerClient(); |
| } |
| |
| TextCheckerClient& SpellChecker::textChecker() const { |
| return spellCheckerClient().textChecker(); |
| } |
| |
| SpellChecker::SpellChecker(LocalFrame& frame) |
| : m_frame(&frame), |
| m_spellCheckRequester(SpellCheckRequester::create(frame)) {} |
| |
| bool SpellChecker::isSpellCheckingEnabled() const { |
| return spellCheckerClient().isSpellCheckingEnabled(); |
| } |
| |
| void SpellChecker::toggleSpellCheckingEnabled() { |
| spellCheckerClient().toggleSpellCheckingEnabled(); |
| if (isSpellCheckingEnabled()) |
| return; |
| for (Frame* frame = this->frame().page()->mainFrame(); frame; |
| frame = frame->tree().traverseNext()) { |
| if (!frame->isLocalFrame()) |
| continue; |
| for (Node& node : |
| NodeTraversal::startsAt(toLocalFrame(frame)->document()->rootNode())) |
| node.setAlreadySpellChecked(false); |
| } |
| } |
| |
| void SpellChecker::didBeginEditing(Element* element) { |
| if (!isSpellCheckingEnabled()) |
| return; |
| |
| // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets |
| // needs to be audited. See http://crbug.com/590369 for more details. |
| // In the long term we should use idle time spell checker to prevent |
| // synchronous layout caused by spell checking (see crbug.com/517298). |
| frame().document()->updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| DocumentLifecycle::DisallowTransitionScope disallowTransition( |
| frame().document()->lifecycle()); |
| |
| bool isTextField = false; |
| HTMLTextFormControlElement* enclosingHTMLTextFormControlElement = 0; |
| if (!isHTMLTextFormControlElement(*element)) |
| enclosingHTMLTextFormControlElement = |
| enclosingTextFormControl(Position::firstPositionInNode(element)); |
| element = enclosingHTMLTextFormControlElement |
| ? enclosingHTMLTextFormControlElement |
| : element; |
| Element* parent = element; |
| if (isHTMLTextFormControlElement(*element)) { |
| HTMLTextFormControlElement* textControl = |
| toHTMLTextFormControlElement(element); |
| parent = textControl; |
| element = textControl->innerEditorElement(); |
| if (!element) |
| return; |
| isTextField = isHTMLInputElement(*textControl) && |
| toHTMLInputElement(*textControl).isTextField(); |
| } |
| |
| if (isTextField || !parent->isAlreadySpellChecked()) { |
| if (EditingStrategy::editingIgnoresContent(element)) |
| return; |
| // We always recheck textfields because markers are removed from them on |
| // blur. |
| VisibleSelection selection = |
| VisibleSelection::selectionFromContentsOfNode(element); |
| markMisspellingsAndBadGrammar(selection); |
| if (!isTextField) |
| parent->setAlreadySpellChecked(true); |
| } |
| } |
| |
| void SpellChecker::ignoreSpelling() { |
| removeMarkers(frame().selection().selection(), DocumentMarker::Spelling); |
| } |
| |
| void SpellChecker::advanceToNextMisspelling(bool startBeforeSelection) { |
| DocumentLifecycle::DisallowTransitionScope disallowTransition( |
| frame().document()->lifecycle()); |
| |
| // 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()); |
| Position spellingSearchStart, spellingSearchEnd; |
| Range::selectNodeContents(frame().document(), spellingSearchStart, |
| spellingSearchEnd); |
| |
| bool startedWithSelection = false; |
| if (selection.start().anchorNode()) { |
| startedWithSelection = true; |
| if (startBeforeSelection) { |
| VisiblePosition start(selection.visibleStart()); |
| // We match AppKit's rule: Start 1 character before the selection. |
| VisiblePosition oneBeforeStart = previousPositionOf(start); |
| spellingSearchStart = |
| (oneBeforeStart.isNotNull() ? oneBeforeStart : start) |
| .toParentAnchoredPosition(); |
| } else { |
| spellingSearchStart = selection.visibleEnd().toParentAnchoredPosition(); |
| } |
| } |
| |
| Position position = spellingSearchStart; |
| 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. |
| |
| if (!frame().document()->documentElement()) |
| return; |
| position = firstEditableVisiblePositionAfterPositionInRoot( |
| position, *frame().document()->documentElement()) |
| .deepEquivalent(); |
| if (position.isNull()) |
| return; |
| |
| spellingSearchStart = position.parentAnchoredEquivalent(); |
| startedWithSelection = false; // won't need to wrap |
| } |
| |
| // topNode defines the whole range we want to operate on |
| ContainerNode* topNode = highestEditableRoot(position); |
| // TODO(yosin): |lastOffsetForEditing()| is wrong here if |
| // |editingIgnoresContent(highestEditableRoot())| returns true, e.g. <table> |
| spellingSearchEnd = Position::editingPositionOf( |
| topNode, EditingStrategy::lastOffsetForEditing(topNode)); |
| |
| // 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 = |
| previousPositionOf(createVisiblePosition(spellingSearchStart)); |
| if (oneBeforeStart.isNotNull() && |
| rootEditableElementOf(oneBeforeStart) == |
| rootEditableElementOf(spellingSearchStart)) |
| spellingSearchStart = |
| endOfWord(oneBeforeStart).toParentAnchoredPosition(); |
| // else we were already at the start of the editable node |
| } |
| |
| if (spellingSearchStart == spellingSearchEnd) |
| return; // nothing to search in |
| |
| // 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 = spellingSearchEnd.computeContainerNode(); |
| int searchEndOffsetAfterWrap = spellingSearchEnd.offsetInContainerNode(); |
| |
| std::pair<String, int> misspelledItem(String(), 0); |
| String& misspelledWord = misspelledItem.first; |
| int& misspellingOffset = misspelledItem.second; |
| misspelledItem = findFirstMisspelling(spellingSearchStart, spellingSearchEnd); |
| |
| // If we did not find 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) { |
| spellingSearchStart = Position::editingPositionOf(topNode, 0); |
| // going until the end of the very first chunk we tested is far enough |
| spellingSearchEnd = Position::editingPositionOf(searchEndNodeAfterWrap, |
| searchEndOffsetAfterWrap); |
| misspelledItem = |
| findFirstMisspelling(spellingSearchStart, spellingSearchEnd); |
| } |
| |
| if (!misspelledWord.isEmpty()) { |
| // We found a misspelling. Select the misspelling, update the spelling |
| // panel, and store a marker so we draw the red squiggle later. |
| |
| const EphemeralRange misspellingRange = calculateCharacterSubrange( |
| EphemeralRange(spellingSearchStart, spellingSearchEnd), |
| misspellingOffset, misspelledWord.length()); |
| frame().selection().setSelection(createVisibleSelection(misspellingRange)); |
| frame().selection().revealSelection(); |
| spellCheckerClient().updateSpellingUIWithMisspelledWord(misspelledWord); |
| frame().document()->markers().addMarker(misspellingRange.startPosition(), |
| misspellingRange.endPosition(), |
| DocumentMarker::Spelling); |
| } |
| } |
| |
| void SpellChecker::showSpellingGuessPanel() { |
| if (spellCheckerClient().spellingUIIsShowing()) { |
| spellCheckerClient().showSpellingUI(false); |
| return; |
| } |
| |
| advanceToNextMisspelling(true); |
| spellCheckerClient().showSpellingUI(true); |
| } |
| |
| void SpellChecker::clearMisspellingsAndBadGrammarForMovingParagraphs( |
| const VisibleSelection& movingSelection) { |
| removeMarkers(movingSelection, DocumentMarker::MisspellingMarkers()); |
| } |
| |
| void SpellChecker::markMisspellingsAndBadGrammarForMovingParagraphs( |
| const VisibleSelection& movingSelection) { |
| // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets |
| // needs to be audited. See http://crbug.com/590369 for more details. |
| // In the long term we should use idle time spell checker to prevent |
| // synchronous layout caused by spell checking (see crbug.com/517298). |
| frame().document()->updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| DocumentLifecycle::DisallowTransitionScope disallowTransition( |
| frame().document()->lifecycle()); |
| |
| markMisspellingsAndBadGrammar(movingSelection); |
| } |
| |
| void SpellChecker::markMisspellingsAndBadGrammar( |
| const VisibleSelection& selection) { |
| if (!isSpellCheckingEnabled() || !isSpellCheckingEnabledFor(selection)) |
| return; |
| |
| const EphemeralRange& range = selection.toNormalizedEphemeralRange(); |
| if (range.isNull()) |
| return; |
| |
| // If we're not in an editable node, bail. |
| Node* editableNode = range.startPosition().computeContainerNode(); |
| if (!editableNode || !hasEditableStyle(*editableNode)) |
| return; |
| |
| TextCheckingParagraph fullParagraphToCheck( |
| expandRangeToSentenceBoundary(range)); |
| chunkAndMarkAllMisspellingsAndBadGrammar(fullParagraphToCheck); |
| } |
| |
| void SpellChecker::markMisspellingsAfterApplyingCommand( |
| const CompositeEditCommand& cmd) { |
| if (!isSpellCheckingEnabled()) |
| return; |
| if (!isSpellCheckingEnabledFor(cmd.endingSelection())) |
| return; |
| |
| // Use type-based conditioning instead of polymorphism so that all spell |
| // checking code can be encapsulated in SpellChecker. |
| |
| if (cmd.isTypingCommand()) { |
| markMisspellingsAfterTypingCommand(toTypingCommand(cmd)); |
| return; |
| } |
| |
| if (!cmd.isReplaceSelectionCommand()) |
| return; |
| |
| // Note: Request spell checking for and only for |ReplaceSelectionCommand|s |
| // created in |Editor::replaceSelectionWithFragment()|. |
| // TODO(xiaochengh): May also need to do this after dragging crbug.com/298046. |
| if (cmd.inputType() != InputEvent::InputType::InsertFromPaste) |
| return; |
| |
| markMisspellingsAfterReplaceSelectionCommand(toReplaceSelectionCommand(cmd)); |
| } |
| |
| void SpellChecker::markMisspellingsAfterTypingCommand( |
| const TypingCommand& cmd) { |
| m_spellCheckRequester->cancelCheck(); |
| |
| // Take a look at the selection that results after typing and determine |
| // whether we need to spellcheck. Since the word containing the current |
| // selection is never marked, this does a check to see if typing made a new |
| // word that is not in the current selection. Basically, you get this by |
| // being at the end of a word and typing a space. |
| VisiblePosition start = createVisiblePosition( |
| cmd.endingSelection().start(), cmd.endingSelection().affinity()); |
| VisiblePosition previous = previousPositionOf(start); |
| |
| VisiblePosition wordStartOfPrevious = |
| startOfWord(previous, LeftWordIfOnBoundary); |
| |
| if (cmd.commandTypeOfOpenCommand() == |
| TypingCommand::InsertParagraphSeparator) { |
| VisiblePosition nextWord = nextWordPosition(start); |
| VisibleSelection words = |
| createVisibleSelection(wordStartOfPrevious, endOfWord(nextWord)); |
| markMisspellingsAfterLineBreak(words); |
| return; |
| } |
| |
| if (previous.isNull()) |
| return; |
| VisiblePosition currentWordStart = startOfWord(start, LeftWordIfOnBoundary); |
| if (wordStartOfPrevious.deepEquivalent() == currentWordStart.deepEquivalent()) |
| return; |
| markMisspellingsAfterTypingToWord(wordStartOfPrevious); |
| } |
| |
| void SpellChecker::markMisspellingsAfterLineBreak( |
| const VisibleSelection& wordSelection) { |
| TRACE_EVENT0("blink", "SpellChecker::markMisspellingsAfterLineBreak"); |
| |
| markMisspellingsAndBadGrammar(wordSelection); |
| } |
| |
| void SpellChecker::markMisspellingsAfterTypingToWord( |
| const VisiblePosition& wordStart) { |
| TRACE_EVENT0("blink", "SpellChecker::markMisspellingsAfterTypingToWord"); |
| |
| VisibleSelection adjacentWords = |
| createVisibleSelection(startOfWord(wordStart, LeftWordIfOnBoundary), |
| endOfWord(wordStart, RightWordIfOnBoundary)); |
| markMisspellingsAndBadGrammar(adjacentWords); |
| } |
| |
| bool SpellChecker::isSpellCheckingEnabledInFocusedNode() const { |
| Node* focusedNode = frame().selection().start().anchorNode(); |
| if (!focusedNode) |
| return false; |
| const Element* focusedElement = focusedNode->isElementNode() |
| ? toElement(focusedNode) |
| : focusedNode->parentElement(); |
| if (!focusedElement) |
| return false; |
| return focusedElement->isSpellCheckingEnabled(); |
| } |
| |
| void SpellChecker::markMisspellingsAfterReplaceSelectionCommand( |
| const ReplaceSelectionCommand& cmd) { |
| TRACE_EVENT0("blink", |
| "SpellChecker::markMisspellingsAfterReplaceSelectionCommand"); |
| |
| const EphemeralRange& insertedRange = cmd.insertedRange(); |
| if (insertedRange.isNull()) |
| return; |
| |
| Node* node = cmd.endingSelection().rootEditableElement(); |
| if (!node) |
| return; |
| |
| EphemeralRange paragraphRange(Position::firstPositionInNode(node), |
| Position::lastPositionInNode(node)); |
| TextCheckingParagraph textToCheck(insertedRange, paragraphRange); |
| chunkAndMarkAllMisspellingsAndBadGrammar(textToCheck); |
| } |
| |
| void SpellChecker::chunkAndMarkAllMisspellingsAndBadGrammar( |
| const TextCheckingParagraph& fullParagraphToCheck) { |
| if (fullParagraphToCheck.isEmpty()) |
| return; |
| const EphemeralRange& paragraphRange = fullParagraphToCheck.paragraphRange(); |
| |
| // Since the text may be quite big chunk it up and adjust to the sentence |
| // boundary. |
| const int kChunkSize = 16 * 1024; |
| |
| // Check the full paragraph instead if the paragraph is short, which saves |
| // the cost on sentence boundary finding. |
| if (fullParagraphToCheck.rangeLength() <= kChunkSize) { |
| SpellCheckRequest* request = |
| SpellCheckRequest::create(TextCheckingProcessBatch, paragraphRange, 0); |
| if (request) |
| m_spellCheckRequester->requestCheckingFor(request); |
| return; |
| } |
| |
| CharacterIterator checkRangeIterator( |
| fullParagraphToCheck.checkingRange(), |
| TextIteratorEmitsObjectReplacementCharacter); |
| for (int requestNum = 0; !checkRangeIterator.atEnd(); requestNum++) { |
| EphemeralRange chunkRange = |
| checkRangeIterator.calculateCharacterSubrange(0, kChunkSize); |
| EphemeralRange checkRange = requestNum |
| ? expandEndToSentenceBoundary(chunkRange) |
| : expandRangeToSentenceBoundary(chunkRange); |
| |
| SpellCheckRequest* request = SpellCheckRequest::create( |
| TextCheckingProcessBatch, checkRange, requestNum); |
| if (request) |
| m_spellCheckRequester->requestCheckingFor(request); |
| |
| if (!checkRangeIterator.atEnd()) { |
| checkRangeIterator.advance(1); |
| // The layout should be already update due to the initialization of |
| // checkRangeIterator, so comparePositions can be directly called. |
| if (comparePositions(chunkRange.endPosition(), checkRange.endPosition()) < |
| 0) |
| checkRangeIterator.advance(TextIterator::rangeLength( |
| chunkRange.endPosition(), checkRange.endPosition())); |
| } |
| } |
| } |
| |
| void SpellChecker::markAndReplaceFor( |
| SpellCheckRequest* request, |
| const Vector<TextCheckingResult>& results) { |
| TRACE_EVENT0("blink", "SpellChecker::markAndReplaceFor"); |
| DCHECK(request); |
| if (!frame().selection().isAvailable()) { |
| // "editing/spelling/spellcheck-async-remove-frame.html" reaches here. |
| return; |
| } |
| if (!request->isValid()) |
| return; |
| if (request->rootEditableElement()->document() != |
| frame().selection().document()) { |
| // we ignore |request| made for another document. |
| // "editing/spelling/spellcheck-sequencenum.html" and others reach here. |
| return; |
| } |
| |
| // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets |
| // needs to be audited. See http://crbug.com/590369 for more details. |
| frame().document()->updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| TextCheckingParagraph paragraph(request->checkingRange(), |
| request->checkingRange()); |
| |
| // Expand the range to encompass entire paragraphs, since text checking needs |
| // that much context. |
| int selectionOffset = 0; |
| int ambiguousBoundaryOffset = -1; |
| bool selectionChanged = false; |
| bool restoreSelectionAfterChange = false; |
| bool adjustSelectionForParagraphBoundaries = false; |
| |
| { |
| DocumentLifecycle::DisallowTransitionScope disallowTransition( |
| frame().document()->lifecycle()); |
| |
| if (frame().selection().isCaret()) { |
| // Attempt to save the caret position so we can restore it later if needed |
| Position caretPosition = frame().selection().end(); |
| selectionOffset = paragraph.offsetTo(caretPosition); |
| restoreSelectionAfterChange = true; |
| if (selectionOffset > 0 && |
| (static_cast<unsigned>(selectionOffset) > paragraph.text().length() || |
| paragraph.textCharAt(selectionOffset - 1) == newlineCharacter)) |
| adjustSelectionForParagraphBoundaries = true; |
| if (selectionOffset > 0 && |
| static_cast<unsigned>(selectionOffset) <= paragraph.text().length() && |
| isAmbiguousBoundaryCharacter( |
| paragraph.textCharAt(selectionOffset - 1))) |
| ambiguousBoundaryOffset = selectionOffset - 1; |
| } |
| |
| for (unsigned i = 0; i < results.size(); i++) { |
| int spellingRangeEndOffset = paragraph.checkingEnd(); |
| const TextCheckingResult* result = &results[i]; |
| int resultLocation = result->location + paragraph.checkingStart(); |
| int resultLength = result->length; |
| bool resultEndsAtAmbiguousBoundary = |
| ambiguousBoundaryOffset >= 0 && |
| resultLocation + resultLength == ambiguousBoundaryOffset; |
| |
| // Only mark misspelling if: |
| // 1. Current text checking isn't done for autocorrection. |
| // 2. Result falls within spellingRange. |
| // 3. The word in question doesn't end at an ambiguous boundary. For |
| // instance, we would not mark "wouldn'" as misspelled right after |
| // apostrophe is typed. |
| if (result->decoration == TextDecorationTypeSpelling && |
| resultLocation >= paragraph.checkingStart() && |
| resultLocation + resultLength <= spellingRangeEndOffset && |
| !resultEndsAtAmbiguousBoundary) { |
| DCHECK_GT(resultLength, 0); |
| DCHECK_GE(resultLocation, 0); |
| const EphemeralRange misspellingRange = calculateCharacterSubrange( |
| paragraph.paragraphRange(), resultLocation, resultLength); |
| frame().document()->markers().addMarker( |
| misspellingRange.startPosition(), misspellingRange.endPosition(), |
| DocumentMarker::Spelling, result->replacement, result->hash); |
| } else if (result->decoration == TextDecorationTypeGrammar && |
| paragraph.checkingRangeCovers(resultLocation, resultLength)) { |
| DCHECK_GT(resultLength, 0); |
| DCHECK_GE(resultLocation, 0); |
| for (unsigned j = 0; j < result->details.size(); j++) { |
| const GrammarDetail* detail = &result->details[j]; |
| DCHECK_GT(detail->length, 0); |
| DCHECK_GE(detail->location, 0); |
| if (paragraph.checkingRangeCovers(resultLocation + detail->location, |
| detail->length)) { |
| const EphemeralRange badGrammarRange = calculateCharacterSubrange( |
| paragraph.paragraphRange(), resultLocation + detail->location, |
| detail->length); |
| frame().document()->markers().addMarker( |
| badGrammarRange.startPosition(), badGrammarRange.endPosition(), |
| DocumentMarker::Grammar, detail->userDescription, result->hash); |
| } |
| } |
| } else if (result->decoration == TextDecorationTypeInvisibleSpellcheck && |
| resultLocation >= paragraph.checkingStart() && |
| resultLocation + resultLength <= spellingRangeEndOffset) { |
| DCHECK_GT(resultLength, 0); |
| DCHECK_GE(resultLocation, 0); |
| const EphemeralRange invisibleSpellcheckRange = |
| calculateCharacterSubrange(paragraph.paragraphRange(), |
| resultLocation, resultLength); |
| frame().document()->markers().addMarker( |
| invisibleSpellcheckRange.startPosition(), |
| invisibleSpellcheckRange.endPosition(), |
| DocumentMarker::InvisibleSpellcheck, result->replacement, |
| result->hash); |
| } |
| } |
| } |
| |
| if (selectionChanged) { |
| TextCheckingParagraph extendedParagraph(paragraph); |
| // Restore the caret position if we have made any replacements |
| extendedParagraph.expandRangeToNextEnd(); |
| if (restoreSelectionAfterChange && selectionOffset >= 0 && |
| selectionOffset <= extendedParagraph.rangeLength()) { |
| EphemeralRange selectionRange = |
| extendedParagraph.subrange(0, selectionOffset); |
| frame().selection().moveTo(selectionRange.endPosition(), |
| TextAffinity::Downstream); |
| if (adjustSelectionForParagraphBoundaries) |
| frame().selection().modify(FrameSelection::AlterationMove, |
| DirectionForward, CharacterGranularity); |
| } else { |
| // If this fails for any reason, the fallback is to go one position beyond |
| // the last replacement |
| frame().selection().moveTo(frame().selection().selection().visibleEnd()); |
| frame().selection().modify(FrameSelection::AlterationMove, |
| DirectionForward, CharacterGranularity); |
| } |
| } |
| } |
| |
| void SpellChecker::updateMarkersForWordsAffectedByEditing( |
| bool doNotRemoveIfSelectionAtWordBoundary) { |
| DCHECK(frame().selection().isAvailable()); |
| TRACE_EVENT0("blink", "SpellChecker::updateMarkersForWordsAffectedByEditing"); |
| if (!isSpellCheckingEnabledFor(frame().selection().selection())) |
| return; |
| |
| Document* document = frame().document(); |
| DCHECK(document); |
| |
| // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets |
| // needs to be audited. See http://crbug.com/590369 for more details. |
| document->updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| // We want to remove the markers from a word if an editing command will change |
| // the word. This can happen in one of several scenarios: |
| // 1. Insert in the middle of a word. |
| // 2. Appending non whitespace at the beginning of word. |
| // 3. Appending non whitespace at the end of word. |
| // Note that, appending only whitespaces at the beginning or end of word won't |
| // change the word, so we don't need to remove the markers on that word. Of |
| // course, if current selection is a range, we potentially will edit two words |
| // that fall on the boundaries of selection, and remove words between the |
| // selection boundaries. |
| VisiblePosition startOfSelection = |
| frame().selection().selection().visibleStart(); |
| VisiblePosition endOfSelection = frame().selection().selection().visibleEnd(); |
| if (startOfSelection.isNull()) |
| return; |
| // First word is the word that ends after or on the start of selection. |
| VisiblePosition startOfFirstWord = |
| startOfWord(startOfSelection, LeftWordIfOnBoundary); |
| VisiblePosition endOfFirstWord = |
| endOfWord(startOfSelection, LeftWordIfOnBoundary); |
| // Last word is the word that begins before or on the end of selection |
| VisiblePosition startOfLastWord = |
| startOfWord(endOfSelection, RightWordIfOnBoundary); |
| VisiblePosition endOfLastWord = |
| endOfWord(endOfSelection, RightWordIfOnBoundary); |
| |
| if (startOfFirstWord.isNull()) { |
| startOfFirstWord = startOfWord(startOfSelection, RightWordIfOnBoundary); |
| endOfFirstWord = endOfWord(startOfSelection, RightWordIfOnBoundary); |
| } |
| |
| if (endOfLastWord.isNull()) { |
| startOfLastWord = startOfWord(endOfSelection, LeftWordIfOnBoundary); |
| endOfLastWord = endOfWord(endOfSelection, LeftWordIfOnBoundary); |
| } |
| |
| // If doNotRemoveIfSelectionAtWordBoundary is true, and first word ends at the |
| // start of selection, we choose next word as the first word. |
| if (doNotRemoveIfSelectionAtWordBoundary && |
| endOfFirstWord.deepEquivalent() == startOfSelection.deepEquivalent()) { |
| startOfFirstWord = nextWordPosition(startOfFirstWord); |
| endOfFirstWord = endOfWord(startOfFirstWord, RightWordIfOnBoundary); |
| if (startOfFirstWord.deepEquivalent() == endOfSelection.deepEquivalent()) |
| return; |
| } |
| |
| // If doNotRemoveIfSelectionAtWordBoundary is true, and last word begins at |
| // the end of selection, we choose previous word as the last word. |
| if (doNotRemoveIfSelectionAtWordBoundary && |
| startOfLastWord.deepEquivalent() == endOfSelection.deepEquivalent()) { |
| startOfLastWord = previousWordPosition(startOfLastWord); |
| endOfLastWord = endOfWord(startOfLastWord, RightWordIfOnBoundary); |
| if (endOfLastWord.deepEquivalent() == startOfSelection.deepEquivalent()) |
| return; |
| } |
| |
| if (startOfFirstWord.isNull() || endOfFirstWord.isNull() || |
| startOfLastWord.isNull() || endOfLastWord.isNull()) |
| return; |
| |
| const Position& removeMarkerStart = startOfFirstWord.deepEquivalent(); |
| const Position& removeMarkerEnd = endOfLastWord.deepEquivalent(); |
| if (removeMarkerStart > removeMarkerEnd) { |
| // editing/inserting/insert-br-008.html and more reach here. |
| // TODO(yosin): To avoid |DCHECK(removeMarkerStart <= removeMarkerEnd)| |
| // in |EphemeralRange| constructor, we have this if-statement. Once we |
| // fix |startOfWord()| and |endOfWord()|, we should remove this |
| // if-statement. |
| return; |
| } |
| |
| // Now we remove markers on everything between startOfFirstWord and |
| // endOfLastWord. However, if an autocorrection change a single word to |
| // multiple words, we want to remove correction mark from all the resulted |
| // words even we only edit one of them. For example, assuming autocorrection |
| // changes "avantgarde" to "avant garde", we will have CorrectionIndicator |
| // marker on both words and on the whitespace between them. If we then edit |
| // garde, we would like to remove the marker from word "avant" and whitespace |
| // as well. So we need to get the continous range of of marker that contains |
| // the word in question, and remove marker on that whole range. |
| const EphemeralRange wordRange(removeMarkerStart, removeMarkerEnd); |
| document->markers().removeMarkers( |
| wordRange, DocumentMarker::MisspellingMarkers(), |
| DocumentMarkerController::RemovePartiallyOverlappingMarker); |
| } |
| |
| void SpellChecker::didEndEditingOnTextField(Element* e) { |
| TRACE_EVENT0("blink", "SpellChecker::didEndEditingOnTextField"); |
| |
| // Remove markers when deactivating a selection in an <input type="text"/>. |
| // Prevent new ones from appearing too. |
| m_spellCheckRequester->cancelCheck(); |
| HTMLTextFormControlElement* textFormControlElement = |
| toHTMLTextFormControlElement(e); |
| HTMLElement* innerEditor = textFormControlElement->innerEditorElement(); |
| DocumentMarker::MarkerTypes markerTypes(DocumentMarker::Spelling); |
| markerTypes.add(DocumentMarker::Grammar); |
| for (Node& node : NodeTraversal::inclusiveDescendantsOf(*innerEditor)) |
| frame().document()->markers().removeMarkers(&node, markerTypes); |
| } |
| |
| void SpellChecker::replaceMisspelledRange(const String& text) { |
| EphemeralRange caretRange = |
| frame().selection().selection().toNormalizedEphemeralRange(); |
| if (caretRange.isNull()) |
| return; |
| DocumentMarkerVector markers = frame().document()->markers().markersInRange( |
| caretRange, DocumentMarker::MisspellingMarkers()); |
| if (markers.size() < 1 || |
| markers[0]->startOffset() >= markers[0]->endOffset()) |
| return; |
| EphemeralRange markerRange = |
| EphemeralRange(Position(caretRange.startPosition().computeContainerNode(), |
| markers[0]->startOffset()), |
| Position(caretRange.endPosition().computeContainerNode(), |
| markers[0]->endOffset())); |
| if (markerRange.isNull()) |
| return; |
| frame().selection().setSelection(createVisibleSelection(markerRange), |
| CharacterGranularity); |
| |
| // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets |
| // needs to be audited. See http://crbug.com/590369 for more details. |
| frame().document()->updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| frame().editor().replaceSelectionWithText(text, false, false); |
| } |
| |
| static bool shouldCheckOldSelection(const VisibleSelection& oldSelection) { |
| if (!oldSelection.start().isConnected()) |
| return false; |
| if (isSelectionInTextField(oldSelection)) |
| return false; |
| if (isSelectionInTextArea(oldSelection)) |
| return true; |
| |
| // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets |
| // needs to be audited. See http://crbug.com/590369 for more details. |
| // In the long term we should use idle time spell checker to prevent |
| // synchronous layout caused by spell checking (see crbug.com/517298). |
| oldSelection.start() |
| .document() |
| ->updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| return oldSelection.isContentEditable(); |
| } |
| |
| void SpellChecker::respondToChangedSelection( |
| const VisibleSelection& oldSelection, |
| FrameSelection::SetSelectionOptions options) { |
| TRACE_EVENT0("blink", "SpellChecker::respondToChangedSelection"); |
| if (!isSpellCheckingEnabledFor(oldSelection)) |
| return; |
| |
| // When spell checking is off, existing markers disappear after the selection |
| // changes. |
| if (!isSpellCheckingEnabled()) { |
| frame().document()->markers().removeMarkers(DocumentMarker::Spelling); |
| frame().document()->markers().removeMarkers(DocumentMarker::Grammar); |
| return; |
| } |
| |
| if (!(options & FrameSelection::CloseTyping)) |
| return; |
| if (!shouldCheckOldSelection(oldSelection)) |
| return; |
| |
| // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets |
| // needs to be audited. See http://crbug.com/590369 for more details. |
| // In the long term we should use idle time spell checker to prevent |
| // synchronous layout caused by spell checking (see crbug.com/517298). |
| frame().document()->updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| DocumentLifecycle::DisallowTransitionScope disallowTransition( |
| frame().document()->lifecycle()); |
| |
| VisibleSelection newAdjacentWords; |
| const VisibleSelection newSelection = frame().selection().selection(); |
| if (isSelectionInTextFormControl(newSelection)) { |
| const Position newStart = newSelection.start(); |
| newAdjacentWords.setWithoutValidation( |
| HTMLTextFormControlElement::startOfWord(newStart), |
| HTMLTextFormControlElement::endOfWord(newStart)); |
| } else { |
| const bool caretBrowsing = |
| frame().settings() && frame().settings()->caretBrowsingEnabled(); |
| if (newSelection.isContentEditable() || caretBrowsing) { |
| const VisiblePosition newStart(newSelection.visibleStart()); |
| newAdjacentWords = |
| createVisibleSelection(startOfWord(newStart, LeftWordIfOnBoundary), |
| endOfWord(newStart, RightWordIfOnBoundary)); |
| } |
| } |
| |
| // When typing we check spelling elsewhere, so don't redo it here. |
| // If this is a change in selection resulting from a delete operation, |
| // oldSelection may no longer be in the document. |
| // FIXME(http://crbug.com/382809): if oldSelection is on a textarea |
| // element, we cause synchronous layout. |
| spellCheckOldSelection(oldSelection, newAdjacentWords); |
| } |
| |
| void SpellChecker::removeSpellingMarkers() { |
| frame().document()->markers().removeMarkers( |
| DocumentMarker::MisspellingMarkers()); |
| } |
| |
| void SpellChecker::removeSpellingMarkersUnderWords( |
| const Vector<String>& words) { |
| MarkerRemoverPredicate removerPredicate(words); |
| |
| DocumentMarkerController& markerController = frame().document()->markers(); |
| markerController.removeMarkers(removerPredicate); |
| markerController.repaintMarkers(); |
| } |
| |
| void SpellChecker::spellCheckAfterBlur() { |
| if (!frame().selection().selection().isContentEditable()) |
| return; |
| |
| if (isSelectionInTextField(frame().selection().selection())) { |
| // textFieldDidEndEditing() and textFieldDidBeginEditing() handle this. |
| return; |
| } |
| |
| // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets |
| // needs to be audited. See http://crbug.com/590369 for more details. |
| // In the long term we should use idle time spell checker to prevent |
| // synchronous layout caused by spell checking (see crbug.com/517298). |
| frame().document()->updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| DocumentLifecycle::DisallowTransitionScope disallowTransition( |
| frame().document()->lifecycle()); |
| |
| VisibleSelection empty; |
| spellCheckOldSelection(frame().selection().selection(), empty); |
| } |
| |
| void SpellChecker::spellCheckOldSelection( |
| const VisibleSelection& oldSelection, |
| const VisibleSelection& newAdjacentWords) { |
| if (!isSpellCheckingEnabled()) |
| return; |
| |
| TRACE_EVENT0("blink", "SpellChecker::spellCheckOldSelection"); |
| |
| VisiblePosition oldStart(oldSelection.visibleStart()); |
| VisibleSelection oldAdjacentWords = |
| createVisibleSelection(startOfWord(oldStart, LeftWordIfOnBoundary), |
| endOfWord(oldStart, RightWordIfOnBoundary)); |
| if (oldAdjacentWords == newAdjacentWords) |
| return; |
| markMisspellingsAndBadGrammar(oldAdjacentWords); |
| } |
| |
| static Node* findFirstMarkable(Node* node) { |
| while (node) { |
| if (!node->layoutObject()) |
| return 0; |
| if (node->layoutObject()->isText()) |
| return node; |
| if (node->layoutObject()->isTextControl()) |
| node = toLayoutTextControl(node->layoutObject()) |
| ->textFormControlElement() |
| ->visiblePositionForIndex(1) |
| .deepEquivalent() |
| .anchorNode(); |
| else if (node->hasChildren()) |
| node = node->firstChild(); |
| else |
| node = node->nextSibling(); |
| } |
| |
| return 0; |
| } |
| |
| bool SpellChecker::selectionStartHasMarkerFor( |
| DocumentMarker::MarkerType markerType, |
| int from, |
| int length) const { |
| Node* node = findFirstMarkable(frame().selection().start().anchorNode()); |
| if (!node) |
| return false; |
| |
| unsigned startOffset = static_cast<unsigned>(from); |
| unsigned endOffset = static_cast<unsigned>(from + length); |
| DocumentMarkerVector markers = frame().document()->markers().markersFor(node); |
| for (size_t i = 0; i < markers.size(); ++i) { |
| DocumentMarker* marker = markers[i]; |
| if (marker->startOffset() <= startOffset && |
| endOffset <= marker->endOffset() && marker->type() == markerType) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool SpellChecker::selectionStartHasSpellingMarkerFor(int from, |
| int length) const { |
| return selectionStartHasMarkerFor(DocumentMarker::Spelling, from, length); |
| } |
| |
| void SpellChecker::removeMarkers(const VisibleSelection& selection, |
| DocumentMarker::MarkerTypes markerTypes) { |
| const EphemeralRange range = selection.toNormalizedEphemeralRange(); |
| if (range.isNull()) |
| return; |
| |
| // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets |
| // needs to be audited. See http://crbug.com/590369 for more details. |
| frame().document()->updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| frame().document()->markers().removeMarkers(range, markerTypes); |
| } |
| |
| void SpellChecker::cancelCheck() { |
| m_spellCheckRequester->cancelCheck(); |
| } |
| |
| void SpellChecker::requestTextChecking(const Element& element) { |
| if (!element.isSpellCheckingEnabled()) |
| return; |
| const EphemeralRange rangeToCheck = EphemeralRange::rangeOfContents(element); |
| m_spellCheckRequester->requestCheckingFor( |
| SpellCheckRequest::create(TextCheckingProcessBatch, rangeToCheck)); |
| } |
| |
| DEFINE_TRACE(SpellChecker) { |
| visitor->trace(m_frame); |
| visitor->trace(m_spellCheckRequester); |
| } |
| |
| void SpellChecker::prepareForLeakDetection() { |
| m_spellCheckRequester->prepareForLeakDetection(); |
| } |
| |
| Vector<TextCheckingResult> SpellChecker::findMisspellings(const String& text) { |
| Vector<UChar> characters; |
| text.appendTo(characters); |
| unsigned length = text.length(); |
| |
| TextBreakIterator* iterator = wordBreakIterator(characters.data(), length); |
| if (!iterator) |
| return Vector<TextCheckingResult>(); |
| |
| Vector<TextCheckingResult> results; |
| int wordStart = iterator->current(); |
| while (wordStart >= 0) { |
| int wordEnd = iterator->next(); |
| if (wordEnd < 0) |
| break; |
| int wordLength = wordEnd - wordStart; |
| int misspellingLocation = -1; |
| int misspellingLength = 0; |
| textChecker().checkSpellingOfString( |
| String(characters.data() + wordStart, wordLength), &misspellingLocation, |
| &misspellingLength); |
| if (misspellingLength > 0) { |
| DCHECK_GE(misspellingLocation, 0); |
| DCHECK_LE(misspellingLocation + misspellingLength, wordLength); |
| TextCheckingResult misspelling; |
| misspelling.decoration = TextDecorationTypeSpelling; |
| misspelling.location = wordStart + misspellingLocation; |
| misspelling.length = misspellingLength; |
| results.append(misspelling); |
| } |
| wordStart = wordEnd; |
| } |
| return results; |
| } |
| |
| std::pair<String, int> SpellChecker::findFirstMisspelling(const Position& start, |
| const Position& end) { |
| String misspelledWord; |
| |
| // Initialize out parameters; they will be updated if we find something to |
| // return. |
| String firstFoundItem; |
| int firstFoundOffset = 0; |
| |
| // 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. |
| Position paragraphStart = |
| startOfParagraph(createVisiblePosition(start)).toParentAnchoredPosition(); |
| Position paragraphEnd = end; |
| int totalRangeLength = |
| TextIterator::rangeLength(paragraphStart, paragraphEnd); |
| paragraphEnd = |
| endOfParagraph(createVisiblePosition(start)).toParentAnchoredPosition(); |
| |
| int rangeStartOffset = TextIterator::rangeLength(paragraphStart, start); |
| int totalLengthProcessed = 0; |
| |
| bool firstIteration = true; |
| bool lastIteration = false; |
| while (totalLengthProcessed < totalRangeLength) { |
| // Iterate through the search range by paragraphs, checking each one for |
| // spelling. |
| int currentLength = TextIterator::rangeLength(paragraphStart, paragraphEnd); |
| int currentStartOffset = firstIteration ? rangeStartOffset : 0; |
| int currentEndOffset = currentLength; |
| if (inSameParagraph(createVisiblePosition(paragraphStart), |
| createVisiblePosition(end))) { |
| // 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. |
| currentEndOffset = TextIterator::rangeLength(paragraphStart, end); |
| lastIteration = true; |
| } |
| if (currentStartOffset < currentEndOffset) { |
| String paragraphString = |
| plainText(EphemeralRange(paragraphStart, paragraphEnd)); |
| if (paragraphString.length() > 0) { |
| int spellingLocation = 0; |
| |
| Vector<TextCheckingResult> results = findMisspellings(paragraphString); |
| |
| for (unsigned i = 0; i < results.size(); i++) { |
| const TextCheckingResult* result = &results[i]; |
| if (result->location >= currentStartOffset && |
| result->location + result->length <= currentEndOffset) { |
| DCHECK_GT(result->length, 0); |
| DCHECK_GE(result->location, 0); |
| spellingLocation = result->location; |
| misspelledWord = |
| paragraphString.substring(result->location, result->length); |
| DCHECK(misspelledWord.length()); |
| break; |
| } |
| } |
| |
| if (!misspelledWord.isEmpty()) { |
| int spellingOffset = spellingLocation - currentStartOffset; |
| if (!firstIteration) |
| spellingOffset += TextIterator::rangeLength(start, paragraphStart); |
| firstFoundOffset = spellingOffset; |
| firstFoundItem = misspelledWord; |
| break; |
| } |
| } |
| } |
| if (lastIteration || |
| totalLengthProcessed + currentLength >= totalRangeLength) |
| break; |
| VisiblePosition newParagraphStart = |
| startOfNextParagraph(createVisiblePosition(paragraphEnd)); |
| paragraphStart = newParagraphStart.toParentAnchoredPosition(); |
| paragraphEnd = endOfParagraph(newParagraphStart).toParentAnchoredPosition(); |
| firstIteration = false; |
| totalLengthProcessed += currentLength; |
| } |
| return std::make_pair(firstFoundItem, firstFoundOffset); |
| } |
| |
| } // namespace blink |