| /* |
| * 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 "config.h" |
| #include "core/editing/SpellChecker.h" |
| |
| #include "HTMLNames.h" |
| #include "core/dom/Document.h" |
| #include "core/dom/DocumentMarkerController.h" |
| #include "core/dom/Element.h" |
| #include "core/dom/NodeTraversal.h" |
| #include "core/editing/Editor.h" |
| #include "core/editing/SpellCheckRequester.h" |
| #include "core/editing/TextCheckingHelper.h" |
| #include "core/editing/VisibleUnits.h" |
| #include "core/editing/htmlediting.h" |
| #include "core/frame/Frame.h" |
| #include "core/html/HTMLInputElement.h" |
| #include "core/loader/EmptyClients.h" |
| #include "core/page/Page.h" |
| #include "core/page/Settings.h" |
| #include "core/page/SpellCheckerClient.h" |
| #include "core/rendering/RenderTextControl.h" |
| #include "platform/text/TextCheckerClient.h" |
| |
| namespace WebCore { |
| |
| using namespace HTMLNames; |
| |
| namespace { |
| |
| bool isSelectionInTextField(const VisibleSelection& selection) |
| { |
| HTMLTextFormControlElement* textControl = enclosingTextFormControl(selection.start()); |
| return textControl && textControl->hasTagName(inputTag) && toHTMLInputElement(textControl)->isTextField(); |
| } |
| |
| } // namespace |
| |
| PassOwnPtr<SpellChecker> SpellChecker::create(Frame& frame) |
| { |
| return adoptPtr(new SpellChecker(frame)); |
| } |
| |
| static SpellCheckerClient& emptySpellCheckerClient() |
| { |
| DEFINE_STATIC_LOCAL(EmptySpellCheckerClient, client, ()); |
| return client; |
| } |
| |
| SpellCheckerClient& SpellChecker::spellCheckerClient() const |
| { |
| if (Page* page = m_frame.page()) |
| return page->spellCheckerClient(); |
| return emptySpellCheckerClient(); |
| } |
| |
| TextCheckerClient& SpellChecker::textChecker() const |
| { |
| return spellCheckerClient().textChecker(); |
| } |
| |
| SpellChecker::SpellChecker(Frame& frame) |
| : m_frame(frame) |
| , m_spellCheckRequester(adoptPtr(new SpellCheckRequester(frame))) |
| { |
| } |
| |
| SpellChecker::~SpellChecker() |
| { |
| } |
| |
| bool SpellChecker::isContinuousSpellCheckingEnabled() const |
| { |
| return spellCheckerClient().isContinuousSpellCheckingEnabled(); |
| } |
| |
| void SpellChecker::toggleContinuousSpellChecking() |
| { |
| spellCheckerClient().toggleContinuousSpellChecking(); |
| if (isContinuousSpellCheckingEnabled()) |
| return; |
| for (Frame* frame = m_frame.page()->mainFrame(); frame && frame->document(); frame = frame->tree().traverseNext()) { |
| for (Node* node = frame->document()->rootNode(); node; node = NodeTraversal::next(*node)) { |
| node->setAlreadySpellChecked(false); |
| } |
| } |
| } |
| |
| bool SpellChecker::isGrammarCheckingEnabled() |
| { |
| return spellCheckerClient().isGrammarCheckingEnabled(); |
| } |
| |
| void SpellChecker::didBeginEditing(Element* element) |
| { |
| if (isContinuousSpellCheckingEnabled() && unifiedTextCheckerEnabled()) { |
| bool isTextField = false; |
| HTMLTextFormControlElement* enclosingHTMLTextFormControlElement = 0; |
| if (!isHTMLTextFormControlElement(element)) |
| enclosingHTMLTextFormControlElement = enclosingTextFormControl(firstPositionInNode(element)); |
| element = enclosingHTMLTextFormControlElement ? enclosingHTMLTextFormControlElement : element; |
| Element* parent = element; |
| if (isHTMLTextFormControlElement(element)) { |
| HTMLTextFormControlElement* textControl = toHTMLTextFormControlElement(element); |
| parent = textControl; |
| element = textControl->innerTextElement(); |
| isTextField = textControl->hasTagName(inputTag) && toHTMLInputElement(textControl)->isTextField(); |
| } |
| |
| if (isTextField || !parent->isAlreadySpellChecked()) { |
| // 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() |
| { |
| if (RefPtr<Range> selectedRange = m_frame.selection().toNormalizedRange()) |
| m_frame.document()->markers()->removeMarkers(selectedRange.get(), DocumentMarker::Spelling); |
| } |
| |
| void SpellChecker::advanceToNextMisspelling(bool startBeforeSelection) |
| { |
| // 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(m_frame.selection().selection()); |
| RefPtr<Range> spellingSearchRange(rangeOfContents(m_frame.document())); |
| |
| bool startedWithSelection = false; |
| if (selection.start().deprecatedNode()) { |
| 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, m_frame.document()->documentElement()).deepEquivalent(); |
| if (position.isNull()) |
| return; |
| |
| Position rangeCompliantPosition = position.parentAnchoredEquivalent(); |
| spellingSearchRange->setStart(rangeCompliantPosition.deprecatedNode(), rangeCompliantPosition.deprecatedEditingOffset(), IGNORE_EXCEPTION); |
| 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 <table>) |
| spellingSearchRange->setEnd(topNode, lastOffsetForEditing(topNode), IGNORE_EXCEPTION); |
| |
| // 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(IGNORE_EXCEPTION)) |
| 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 = spellingSearchRange->endContainer(); |
| int searchEndOffsetAfterWrap = spellingSearchRange->endOffset(); |
| |
| int misspellingOffset = 0; |
| GrammarDetail grammarDetail; |
| int grammarPhraseOffset = 0; |
| RefPtr<Range> grammarSearchRange; |
| String badGrammarPhrase; |
| String misspelledWord; |
| |
| bool isSpelling = true; |
| int foundOffset = 0; |
| String foundItem; |
| RefPtr<Range> firstMisspellingRange; |
| if (unifiedTextCheckerEnabled()) { |
| grammarSearchRange = spellingSearchRange->cloneRange(IGNORE_EXCEPTION); |
| foundItem = TextCheckingHelper(spellCheckerClient(), spellingSearchRange).findFirstMisspellingOrBadGrammar(isGrammarCheckingEnabled(), isSpelling, foundOffset, grammarDetail); |
| if (isSpelling) { |
| misspelledWord = foundItem; |
| misspellingOffset = foundOffset; |
| } else { |
| badGrammarPhrase = foundItem; |
| grammarPhraseOffset = foundOffset; |
| } |
| } else { |
| misspelledWord = TextCheckingHelper(spellCheckerClient(), spellingSearchRange).findFirstMisspelling(misspellingOffset, false, firstMisspellingRange); |
| grammarSearchRange = spellingSearchRange->cloneRange(IGNORE_EXCEPTION); |
| if (!misspelledWord.isEmpty()) { |
| // Stop looking at start of next misspelled word |
| CharacterIterator chars(grammarSearchRange.get()); |
| chars.advance(misspellingOffset); |
| grammarSearchRange->setEnd(chars.range()->startContainer(), chars.range()->startOffset(), IGNORE_EXCEPTION); |
| } |
| |
| if (isGrammarCheckingEnabled()) |
| badGrammarPhrase = TextCheckingHelper(spellCheckerClient(), grammarSearchRange).findFirstBadGrammar(grammarDetail, grammarPhraseOffset, false); |
| } |
| |
| // 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, IGNORE_EXCEPTION); |
| // going until the end of the very first chunk we tested is far enough |
| spellingSearchRange->setEnd(searchEndNodeAfterWrap, searchEndOffsetAfterWrap, IGNORE_EXCEPTION); |
| |
| if (unifiedTextCheckerEnabled()) { |
| grammarSearchRange = spellingSearchRange->cloneRange(IGNORE_EXCEPTION); |
| foundItem = TextCheckingHelper(spellCheckerClient(), spellingSearchRange).findFirstMisspellingOrBadGrammar(isGrammarCheckingEnabled(), isSpelling, foundOffset, grammarDetail); |
| if (isSpelling) { |
| misspelledWord = foundItem; |
| misspellingOffset = foundOffset; |
| } else { |
| badGrammarPhrase = foundItem; |
| grammarPhraseOffset = foundOffset; |
| } |
| } else { |
| misspelledWord = TextCheckingHelper(spellCheckerClient(), spellingSearchRange).findFirstMisspelling(misspellingOffset, false, firstMisspellingRange); |
| grammarSearchRange = spellingSearchRange->cloneRange(IGNORE_EXCEPTION); |
| if (!misspelledWord.isEmpty()) { |
| // Stop looking at start of next misspelled word |
| CharacterIterator chars(grammarSearchRange.get()); |
| chars.advance(misspellingOffset); |
| grammarSearchRange->setEnd(chars.range()->startContainer(), chars.range()->startOffset(), IGNORE_EXCEPTION); |
| } |
| |
| if (isGrammarCheckingEnabled()) |
| badGrammarPhrase = TextCheckingHelper(spellCheckerClient(), grammarSearchRange).findFirstBadGrammar(grammarDetail, grammarPhraseOffset, false); |
| } |
| } |
| |
| if (!badGrammarPhrase.isEmpty()) { |
| // 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<Range> badGrammarRange = TextIterator::subrange(grammarSearchRange.get(), grammarPhraseOffset + grammarDetail.location, grammarDetail.length); |
| m_frame.selection().setSelection(VisibleSelection(badGrammarRange.get(), SEL_DEFAULT_AFFINITY)); |
| m_frame.selection().revealSelection(); |
| |
| m_frame.document()->markers()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, grammarDetail.userDescription); |
| } 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<Range> misspellingRange = TextIterator::subrange(spellingSearchRange.get(), misspellingOffset, misspelledWord.length()); |
| m_frame.selection().setSelection(VisibleSelection(misspellingRange.get(), DOWNSTREAM)); |
| m_frame.selection().revealSelection(); |
| |
| spellCheckerClient().updateSpellingUIWithMisspelledWord(misspelledWord); |
| m_frame.document()->markers()->addMarker(misspellingRange.get(), DocumentMarker::Spelling); |
| } |
| } |
| |
| String SpellChecker::misspelledWordAtCaretOrRange(Node* clickedNode) const |
| { |
| if (!isContinuousSpellCheckingEnabled() || !clickedNode || !isSpellCheckingEnabledFor(clickedNode)) |
| return String(); |
| |
| VisibleSelection selection = m_frame.selection().selection(); |
| if (!selection.isContentEditable() || selection.isNone()) |
| return String(); |
| |
| VisibleSelection wordSelection(selection.base()); |
| wordSelection.expandUsingGranularity(WordGranularity); |
| RefPtr<Range> wordRange = wordSelection.toNormalizedRange(); |
| |
| // In compliance with GTK+ applications, additionally allow to provide suggestions when the current |
| // selection exactly match the word selection. |
| if (selection.isRange() && !areRangesEqual(wordRange.get(), selection.toNormalizedRange().get())) |
| return String(); |
| |
| String word = wordRange->text(); |
| if (word.isEmpty()) |
| return String(); |
| |
| int wordLength = word.length(); |
| int misspellingLocation = -1; |
| int misspellingLength = 0; |
| textChecker().checkSpellingOfString(word, &misspellingLocation, &misspellingLength); |
| |
| return misspellingLength == wordLength ? word : String(); |
| } |
| |
| void SpellChecker::showSpellingGuessPanel() |
| { |
| if (spellCheckerClient().spellingUIIsShowing()) { |
| spellCheckerClient().showSpellingUI(false); |
| return; |
| } |
| |
| advanceToNextMisspelling(true); |
| spellCheckerClient().showSpellingUI(true); |
| } |
| |
| void SpellChecker::clearMisspellingsAndBadGrammar(const VisibleSelection &movingSelection) |
| { |
| RefPtr<Range> selectedRange = movingSelection.toNormalizedRange(); |
| if (selectedRange) |
| m_frame.document()->markers()->removeMarkers(selectedRange.get(), DocumentMarker::MisspellingMarkers()); |
| } |
| |
| void SpellChecker::markMisspellingsAndBadGrammar(const VisibleSelection &movingSelection) |
| { |
| markMisspellingsAndBadGrammar(movingSelection, isContinuousSpellCheckingEnabled() && isGrammarCheckingEnabled(), movingSelection); |
| } |
| |
| void SpellChecker::markMisspellingsAfterTypingToWord(const VisiblePosition &wordStart, const VisibleSelection& selectionAfterTyping) |
| { |
| if (unifiedTextCheckerEnabled()) { |
| TextCheckingTypeMask textCheckingOptions = 0; |
| |
| if (isContinuousSpellCheckingEnabled()) |
| textCheckingOptions |= TextCheckingTypeSpelling; |
| |
| if (!(textCheckingOptions & TextCheckingTypeSpelling)) |
| return; |
| |
| if (isGrammarCheckingEnabled()) |
| textCheckingOptions |= TextCheckingTypeGrammar; |
| |
| VisibleSelection adjacentWords = VisibleSelection(startOfWord(wordStart, LeftWordIfOnBoundary), endOfWord(wordStart, RightWordIfOnBoundary)); |
| if (textCheckingOptions & TextCheckingTypeGrammar) { |
| VisibleSelection selectedSentence = VisibleSelection(startOfSentence(wordStart), endOfSentence(wordStart)); |
| markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, adjacentWords.toNormalizedRange().get(), selectedSentence.toNormalizedRange().get()); |
| } else { |
| markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, adjacentWords.toNormalizedRange().get(), adjacentWords.toNormalizedRange().get()); |
| } |
| return; |
| } |
| |
| if (!isContinuousSpellCheckingEnabled()) |
| return; |
| |
| // Check spelling of one word |
| RefPtr<Range> misspellingRange; |
| markMisspellings(VisibleSelection(startOfWord(wordStart, LeftWordIfOnBoundary), endOfWord(wordStart, RightWordIfOnBoundary)), misspellingRange); |
| |
| // Autocorrect the misspelled word. |
| if (!misspellingRange) |
| return; |
| |
| // Get the misspelled word. |
| const String misspelledWord = plainText(misspellingRange.get()); |
| String autocorrectedString = textChecker().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 != m_frame.selection().selection()) { |
| m_frame.selection().setSelection(newSelection); |
| } |
| |
| m_frame.editor().replaceSelectionWithText(autocorrectedString, false, false); |
| |
| // Reset the charet one character further. |
| m_frame.selection().moveTo(m_frame.selection().end()); |
| m_frame.selection().modify(FrameSelection::AlterationMove, DirectionForward, CharacterGranularity); |
| } |
| |
| if (!isGrammarCheckingEnabled()) |
| return; |
| |
| // Check grammar of entire sentence |
| markBadGrammar(VisibleSelection(startOfSentence(wordStart), endOfSentence(wordStart))); |
| } |
| |
| void SpellChecker::markMisspellingsOrBadGrammar(const VisibleSelection& selection, bool checkSpelling, RefPtr<Range>& 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 (!isContinuousSpellCheckingEnabled()) |
| return; |
| |
| RefPtr<Range> searchRange(selection.toNormalizedRange()); |
| if (!searchRange) |
| return; |
| |
| // If we're not in an editable node, bail. |
| Node* editableNode = searchRange->startContainer(); |
| if (!editableNode || !editableNode->rendererIsEditable()) |
| return; |
| |
| if (!isSpellCheckingEnabledFor(editableNode)) |
| return; |
| |
| TextCheckingHelper checker(spellCheckerClient(), searchRange); |
| if (checkSpelling) |
| checker.markAllMisspellings(firstMisspellingRange); |
| else if (isGrammarCheckingEnabled()) |
| checker.markAllBadGrammar(); |
| } |
| |
| bool SpellChecker::isSpellCheckingEnabledFor(Node* node) const |
| { |
| if (!node) |
| return false; |
| const Element* focusedElement = node->isElementNode() ? toElement(node) : node->parentElement(); |
| if (!focusedElement) |
| return false; |
| return focusedElement->isSpellCheckingEnabled(); |
| } |
| |
| bool SpellChecker::isSpellCheckingEnabledInFocusedNode() const |
| { |
| return isSpellCheckingEnabledFor(m_frame.selection().start().deprecatedNode()); |
| } |
| |
| void SpellChecker::markMisspellings(const VisibleSelection& selection, RefPtr<Range>& firstMisspellingRange) |
| { |
| markMisspellingsOrBadGrammar(selection, true, firstMisspellingRange); |
| } |
| |
| void SpellChecker::markBadGrammar(const VisibleSelection& selection) |
| { |
| RefPtr<Range> firstMisspellingRange; |
| markMisspellingsOrBadGrammar(selection, false, firstMisspellingRange); |
| } |
| |
| void SpellChecker::markAllMisspellingsAndBadGrammarInRanges(TextCheckingTypeMask textCheckingOptions, Range* spellingRange, Range* grammarRange) |
| { |
| ASSERT(unifiedTextCheckerEnabled()); |
| |
| bool shouldMarkGrammar = textCheckingOptions & TextCheckingTypeGrammar; |
| |
| // This function is called with selections already expanded to word boundaries. |
| if (!spellingRange || (shouldMarkGrammar && !grammarRange)) |
| return; |
| |
| // If we're not in an editable node, bail. |
| Node* editableNode = spellingRange->startContainer(); |
| if (!editableNode || !editableNode->rendererIsEditable()) |
| return; |
| |
| if (!isSpellCheckingEnabledFor(editableNode)) |
| return; |
| |
| Range* rangeToCheck = shouldMarkGrammar ? grammarRange : spellingRange; |
| TextCheckingParagraph fullParagraphToCheck(rangeToCheck); |
| |
| bool asynchronous = m_frame.settings() && m_frame.settings()->asynchronousSpellCheckingEnabled(); |
| chunkAndMarkAllMisspellingsAndBadGrammar(textCheckingOptions, fullParagraphToCheck, asynchronous); |
| } |
| |
| void SpellChecker::chunkAndMarkAllMisspellingsAndBadGrammar(Node* node) |
| { |
| if (!node) |
| return; |
| RefPtr<Range> rangeToCheck = Range::create(*m_frame.document(), firstPositionInNode(node), lastPositionInNode(node)); |
| TextCheckingParagraph textToCheck(rangeToCheck, rangeToCheck); |
| bool asynchronous = true; |
| chunkAndMarkAllMisspellingsAndBadGrammar(resolveTextCheckingTypeMask(TextCheckingTypeSpelling | TextCheckingTypeGrammar), textToCheck, asynchronous); |
| } |
| |
| void SpellChecker::chunkAndMarkAllMisspellingsAndBadGrammar(TextCheckingTypeMask textCheckingOptions, const TextCheckingParagraph& fullParagraphToCheck, bool asynchronous) |
| { |
| if (fullParagraphToCheck.isRangeEmpty() || fullParagraphToCheck.isEmpty()) |
| return; |
| |
| // Since the text may be quite big chunk it up and adjust to the sentence boundary. |
| const int kChunkSize = 16 * 1024; |
| int start = fullParagraphToCheck.checkingStart(); |
| int end = fullParagraphToCheck.checkingEnd(); |
| start = std::min(start, end); |
| end = std::max(start, end); |
| const int kNumChunksToCheck = asynchronous ? (end - start + kChunkSize - 1) / (kChunkSize) : 1; |
| int currentChunkStart = start; |
| RefPtr<Range> checkRange = fullParagraphToCheck.checkingRange(); |
| if (kNumChunksToCheck == 1 && asynchronous) { |
| markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, checkRange.get(), checkRange.get(), asynchronous, 0); |
| return; |
| } |
| |
| for (int iter = 0; iter < kNumChunksToCheck; ++iter) { |
| checkRange = fullParagraphToCheck.subrange(currentChunkStart, kChunkSize); |
| setStart(checkRange.get(), startOfSentence(checkRange->startPosition())); |
| setEnd(checkRange.get(), endOfSentence(checkRange->endPosition())); |
| |
| int checkingLength = 0; |
| markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, checkRange.get(), checkRange.get(), asynchronous, iter, &checkingLength); |
| currentChunkStart += checkingLength; |
| } |
| } |
| |
| void SpellChecker::markAllMisspellingsAndBadGrammarInRanges(TextCheckingTypeMask textCheckingOptions, Range* checkRange, Range* paragraphRange, bool asynchronous, int requestNumber, int* checkingLength) |
| { |
| TextCheckingParagraph sentenceToCheck(checkRange, paragraphRange); |
| if (checkingLength) |
| *checkingLength = sentenceToCheck.checkingLength(); |
| |
| RefPtr<SpellCheckRequest> request = SpellCheckRequest::create(resolveTextCheckingTypeMask(textCheckingOptions), TextCheckingProcessBatch, checkRange, paragraphRange, requestNumber); |
| |
| if (asynchronous) { |
| m_spellCheckRequester->requestCheckingFor(request); |
| } else { |
| Vector<TextCheckingResult> results; |
| checkTextOfParagraph(textChecker(), sentenceToCheck.text(), resolveTextCheckingTypeMask(textCheckingOptions), results); |
| markAndReplaceFor(request, results); |
| } |
| } |
| |
| void SpellChecker::markAndReplaceFor(PassRefPtr<SpellCheckRequest> request, const Vector<TextCheckingResult>& results) |
| { |
| ASSERT(request); |
| |
| TextCheckingTypeMask textCheckingOptions = request->data().mask(); |
| TextCheckingParagraph paragraph(request->checkingRange(), request->paragraphRange()); |
| |
| bool shouldMarkSpelling = textCheckingOptions & TextCheckingTypeSpelling; |
| bool shouldMarkGrammar = textCheckingOptions & TextCheckingTypeGrammar; |
| |
| // 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; |
| |
| if (shouldMarkSpelling) { |
| if (m_frame.selection().isCaret()) { |
| // Attempt to save the caret position so we can restore it later if needed |
| Position caretPosition = m_frame.selection().end(); |
| selectionOffset = paragraph.offsetTo(caretPosition, ASSERT_NO_EXCEPTION); |
| 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, in which case shouldMarkSpelling is false. |
| // 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 (shouldMarkSpelling && result->decoration == TextDecorationTypeSpelling && resultLocation >= paragraph.checkingStart() && resultLocation + resultLength <= spellingRangeEndOffset && !resultEndsAtAmbiguousBoundary) { |
| ASSERT(resultLength > 0 && resultLocation >= 0); |
| RefPtr<Range> misspellingRange = paragraph.subrange(resultLocation, resultLength); |
| misspellingRange->startContainer()->document().markers()->addMarker(misspellingRange.get(), DocumentMarker::Spelling, result->replacement, result->hash); |
| } else if (shouldMarkGrammar && result->decoration == TextDecorationTypeGrammar && paragraph.checkingRangeCovers(resultLocation, resultLength)) { |
| 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 (paragraph.checkingRangeCovers(resultLocation + detail->location, detail->length)) { |
| RefPtr<Range> badGrammarRange = paragraph.subrange(resultLocation + detail->location, detail->length); |
| badGrammarRange->startContainer()->document().markers()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription, result->hash); |
| } |
| } |
| } else if (result->decoration == TextDecorationTypeInvisibleSpellcheck && resultLocation >= paragraph.checkingStart() && resultLocation + resultLength <= spellingRangeEndOffset) { |
| ASSERT(resultLength > 0 && resultLocation >= 0); |
| RefPtr<Range> invisibleSpellcheckRange = paragraph.subrange(resultLocation, resultLength); |
| invisibleSpellcheckRange->startContainer()->document().markers()->addMarker(invisibleSpellcheckRange.get(), 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()) { |
| RefPtr<Range> selectionRange = extendedParagraph.subrange(0, selectionOffset); |
| m_frame.selection().moveTo(selectionRange->endPosition(), DOWNSTREAM); |
| if (adjustSelectionForParagraphBoundaries) |
| m_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 |
| m_frame.selection().moveTo(m_frame.selection().end()); |
| m_frame.selection().modify(FrameSelection::AlterationMove, DirectionForward, CharacterGranularity); |
| } |
| } |
| } |
| |
| void SpellChecker::markMisspellingsAndBadGrammar(const VisibleSelection& spellingSelection, bool markGrammar, const VisibleSelection& grammarSelection) |
| { |
| if (unifiedTextCheckerEnabled()) { |
| if (!isContinuousSpellCheckingEnabled()) |
| return; |
| |
| // markMisspellingsAndBadGrammar() is triggered by selection change, in which case we check spelling and grammar, but don't autocorrect misspellings. |
| TextCheckingTypeMask textCheckingOptions = TextCheckingTypeSpelling; |
| if (markGrammar && isGrammarCheckingEnabled()) |
| textCheckingOptions |= TextCheckingTypeGrammar; |
| markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, spellingSelection.toNormalizedRange().get(), grammarSelection.toNormalizedRange().get()); |
| return; |
| } |
| |
| RefPtr<Range> firstMisspellingRange; |
| markMisspellings(spellingSelection, firstMisspellingRange); |
| if (markGrammar) |
| markBadGrammar(grammarSelection); |
| } |
| |
| void SpellChecker::updateMarkersForWordsAffectedByEditing(bool doNotRemoveIfSelectionAtWordBoundary) |
| { |
| if (textChecker().shouldEraseMarkersAfterChangeSelection(TextCheckingTypeSpelling)) |
| return; |
| |
| // 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 = m_frame.selection().selection().start(); |
| VisiblePosition endOfSelection = m_frame.selection().selection().end(); |
| 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 == startOfSelection) { |
| startOfFirstWord = nextWordPosition(startOfFirstWord); |
| endOfFirstWord = endOfWord(startOfFirstWord, RightWordIfOnBoundary); |
| if (startOfFirstWord == endOfSelection) |
| 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 == endOfSelection) { |
| startOfLastWord = previousWordPosition(startOfLastWord); |
| endOfLastWord = endOfWord(startOfLastWord, RightWordIfOnBoundary); |
| if (endOfLastWord == startOfSelection) |
| return; |
| } |
| |
| if (startOfFirstWord.isNull() || endOfFirstWord.isNull() || startOfLastWord.isNull() || endOfLastWord.isNull()) |
| 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. |
| Document* document = m_frame.document(); |
| ASSERT(document); |
| RefPtr<Range> wordRange = Range::create(*document, startOfFirstWord.deepEquivalent(), endOfLastWord.deepEquivalent()); |
| |
| document->markers()->removeMarkers(wordRange.get(), DocumentMarker::MisspellingMarkers(), DocumentMarkerController::RemovePartiallyOverlappingMarker); |
| } |
| |
| void SpellChecker::didEndEditingOnTextField(Element* e) |
| { |
| // 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* innerText = textFormControlElement->innerTextElement(); |
| DocumentMarker::MarkerTypes markerTypes(DocumentMarker::Spelling); |
| if (isGrammarCheckingEnabled() || unifiedTextCheckerEnabled()) |
| markerTypes.add(DocumentMarker::Grammar); |
| for (Node* node = innerText; node; node = NodeTraversal::next(*node, innerText)) { |
| m_frame.document()->markers()->removeMarkers(node, markerTypes); |
| } |
| } |
| |
| void SpellChecker::respondToChangedSelection(const VisibleSelection& oldSelection, FrameSelection::SetSelectionOptions options) |
| { |
| bool closeTyping = options & FrameSelection::CloseTyping; |
| bool isContinuousSpellCheckingEnabled = this->isContinuousSpellCheckingEnabled(); |
| bool isContinuousGrammarCheckingEnabled = isContinuousSpellCheckingEnabled && isGrammarCheckingEnabled(); |
| if (isContinuousSpellCheckingEnabled) { |
| VisibleSelection newAdjacentWords; |
| VisibleSelection newSelectedSentence; |
| bool caretBrowsing = m_frame.settings() && m_frame.settings()->caretBrowsingEnabled(); |
| if (m_frame.selection().selection().isContentEditable() || caretBrowsing) { |
| VisiblePosition newStart(m_frame.selection().selection().visibleStart()); |
| newAdjacentWords = VisibleSelection(startOfWord(newStart, LeftWordIfOnBoundary), endOfWord(newStart, RightWordIfOnBoundary)); |
| if (isContinuousGrammarCheckingEnabled) |
| newSelectedSentence = VisibleSelection(startOfSentence(newStart), endOfSentence(newStart)); |
| } |
| |
| // Don't check spelling and grammar if the change of selection is triggered by spelling correction itself. |
| bool shouldCheckSpellingAndGrammar = !(options & FrameSelection::SpellCorrectionTriggered); |
| |
| // 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. |
| if (shouldCheckSpellingAndGrammar |
| && closeTyping |
| && oldSelection.isContentEditable() |
| && oldSelection.start().inDocument() |
| && !isSelectionInTextField(oldSelection)) { |
| spellCheckOldSelection(oldSelection, newAdjacentWords, newSelectedSentence); |
| } |
| |
| if (textChecker().shouldEraseMarkersAfterChangeSelection(TextCheckingTypeSpelling)) { |
| if (RefPtr<Range> wordRange = newAdjacentWords.toNormalizedRange()) |
| m_frame.document()->markers()->removeMarkers(wordRange.get(), DocumentMarker::Spelling); |
| } |
| if (textChecker().shouldEraseMarkersAfterChangeSelection(TextCheckingTypeGrammar)) { |
| if (RefPtr<Range> sentenceRange = newSelectedSentence.toNormalizedRange()) |
| m_frame.document()->markers()->removeMarkers(sentenceRange.get(), DocumentMarker::Grammar); |
| } |
| } |
| |
| // When continuous spell checking is off, existing markers disappear after the selection changes. |
| if (!isContinuousSpellCheckingEnabled) |
| m_frame.document()->markers()->removeMarkers(DocumentMarker::Spelling); |
| if (!isContinuousGrammarCheckingEnabled) |
| m_frame.document()->markers()->removeMarkers(DocumentMarker::Grammar); |
| } |
| |
| void SpellChecker::spellCheckAfterBlur() |
| { |
| if (!m_frame.selection().selection().isContentEditable()) |
| return; |
| |
| if (isSelectionInTextField(m_frame.selection().selection())) { |
| // textFieldDidEndEditing() and textFieldDidBeginEditing() handle this. |
| return; |
| } |
| |
| VisibleSelection empty; |
| spellCheckOldSelection(m_frame.selection().selection(), empty, empty); |
| } |
| |
| void SpellChecker::spellCheckOldSelection(const VisibleSelection& oldSelection, const VisibleSelection& newAdjacentWords, const VisibleSelection& newSelectedSentence) |
| { |
| VisiblePosition oldStart(oldSelection.visibleStart()); |
| VisibleSelection oldAdjacentWords = VisibleSelection(startOfWord(oldStart, LeftWordIfOnBoundary), endOfWord(oldStart, RightWordIfOnBoundary)); |
| if (oldAdjacentWords != newAdjacentWords) { |
| if (isContinuousSpellCheckingEnabled() && isGrammarCheckingEnabled()) { |
| VisibleSelection selectedSentence = VisibleSelection(startOfSentence(oldStart), endOfSentence(oldStart)); |
| markMisspellingsAndBadGrammar(oldAdjacentWords, selectedSentence != newSelectedSentence, selectedSentence); |
| } else { |
| markMisspellingsAndBadGrammar(oldAdjacentWords, false, oldAdjacentWords); |
| } |
| } |
| } |
| |
| static Node* findFirstMarkable(Node* node) |
| { |
| while (node) { |
| if (!node->renderer()) |
| return 0; |
| if (node->renderer()->isText()) |
| return node; |
| if (node->renderer()->isTextControl()) |
| node = toRenderTextControl(node->renderer())->textFormControlElement()->visiblePositionForIndex(1).deepEquivalent().deprecatedNode(); |
| else if (node->firstChild()) |
| node = node->firstChild(); |
| else |
| node = node->nextSibling(); |
| } |
| |
| return 0; |
| } |
| |
| bool SpellChecker::selectionStartHasMarkerFor(DocumentMarker::MarkerType markerType, int from, int length) const |
| { |
| Node* node = findFirstMarkable(m_frame.selection().start().deprecatedNode()); |
| if (!node) |
| return false; |
| |
| unsigned startOffset = static_cast<unsigned>(from); |
| unsigned endOffset = static_cast<unsigned>(from + length); |
| Vector<DocumentMarker*> markers = m_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; |
| } |
| |
| TextCheckingTypeMask SpellChecker::resolveTextCheckingTypeMask(TextCheckingTypeMask textCheckingOptions) |
| { |
| bool shouldMarkSpelling = textCheckingOptions & TextCheckingTypeSpelling; |
| bool shouldMarkGrammar = textCheckingOptions & TextCheckingTypeGrammar; |
| |
| TextCheckingTypeMask checkingTypes = 0; |
| if (shouldMarkSpelling) |
| checkingTypes |= TextCheckingTypeSpelling; |
| if (shouldMarkGrammar) |
| checkingTypes |= TextCheckingTypeGrammar; |
| |
| return checkingTypes; |
| } |
| |
| bool SpellChecker::unifiedTextCheckerEnabled() const |
| { |
| return WebCore::unifiedTextCheckerEnabled(&m_frame); |
| } |
| |
| void SpellChecker::cancelCheck() |
| { |
| m_spellCheckRequester->cancelCheck(); |
| } |
| |
| void SpellChecker::requestTextChecking(const Element& element) |
| { |
| RefPtr<Range> rangeToCheck = rangeOfContents(const_cast<Element*>(&element)); |
| m_spellCheckRequester->requestCheckingFor(SpellCheckRequest::create(TextCheckingTypeSpelling | TextCheckingTypeGrammar, TextCheckingProcessBatch, rangeToCheck, rangeToCheck)); |
| } |
| |
| |
| } // namespace WebCore |