blob: d2ae2abb08d67c1e2e59ed9a85f4eae331f700e7 [file] [log] [blame]
/*
* 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/clipboard/DataObject.h"
#include "core/dom/Document.h"
#include "core/dom/Element.h"
#include "core/dom/ElementTraversal.h"
#include "core/dom/NodeTraversal.h"
#include "core/dom/Range.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/IdleSpellCheckCallback.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/RuntimeEnabledFeatures.h"
#include "platform/text/TextBreakIterator.h"
#include "platform/text/TextCheckerClient.h"
namespace blink {
using namespace HTMLNames;
namespace {
bool IsPositionInTextField(const Position& selection_start) {
TextControlElement* text_control = EnclosingTextControl(selection_start);
return isHTMLInputElement(text_control) &&
toHTMLInputElement(text_control)->IsTextField();
}
bool IsPositionInTextArea(const Position& position) {
TextControlElement* text_control = EnclosingTextControl(position);
return isHTMLTextAreaElement(text_control);
}
static bool IsSpellCheckingEnabledFor(const VisibleSelection& selection) {
if (selection.IsNone())
return false;
return SpellChecker::IsSpellCheckingEnabledAt(selection.Start());
}
SelectionInDOMTree SelectWord(const VisiblePosition& position) {
// TODO(yosin): We should fix |startOfWord()| and |endOfWord()| not to return
// null position.
const VisiblePosition& start = StartOfWord(position, kLeftWordIfOnBoundary);
const VisiblePosition& end = EndOfWord(position, kRightWordIfOnBoundary);
return SelectionInDOMTree::Builder()
.SetBaseAndExtentDeprecated(start.DeepEquivalent(), end.DeepEquivalent())
.SetAffinity(start.Affinity())
.Build();
}
} // namespace
SpellChecker* SpellChecker::Create(LocalFrame& frame) {
return new SpellChecker(frame);
}
static SpellCheckerClient& GetEmptySpellCheckerClient() {
DEFINE_STATIC_LOCAL(EmptySpellCheckerClient, client, ());
return client;
}
SpellCheckerClient& SpellChecker::GetSpellCheckerClient() const {
if (Page* page = GetFrame().GetPage())
return page->GetSpellCheckerClient();
return GetEmptySpellCheckerClient();
}
TextCheckerClient& SpellChecker::TextChecker() const {
return GetFrame().Client()->GetTextCheckerClient();
}
SpellChecker::SpellChecker(LocalFrame& frame)
: frame_(&frame),
spell_check_requester_(SpellCheckRequester::Create(frame)),
idle_spell_check_callback_(IdleSpellCheckCallback::Create(frame)) {}
bool SpellChecker::IsSpellCheckingEnabled() const {
return GetSpellCheckerClient().IsSpellCheckingEnabled();
}
void SpellChecker::ToggleSpellCheckingEnabled() {
GetSpellCheckerClient().ToggleSpellCheckingEnabled();
if (IsSpellCheckingEnabled())
return;
for (Frame* frame = this->GetFrame().GetPage()->MainFrame(); frame;
frame = frame->Tree().TraverseNext()) {
if (!frame->IsLocalFrame())
continue;
for (Node& node : NodeTraversal::StartsAt(
ToLocalFrame(frame)->GetDocument()->RootNode())) {
if (node.IsElementNode())
ToElement(node).SetAlreadySpellChecked(false);
}
}
}
void SpellChecker::DidBeginEditing(Element* element) {
if (RuntimeEnabledFeatures::idleTimeSpellCheckingEnabled())
return;
if (!IsSpellCheckingEnabled())
return;
// TODO(editing-dev): 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).
GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
DocumentLifecycle::DisallowTransitionScope disallow_transition(
GetFrame().GetDocument()->Lifecycle());
bool is_text_field = false;
TextControlElement* enclosing_text_control_element = nullptr;
if (!IsTextControlElement(*element)) {
enclosing_text_control_element =
EnclosingTextControl(Position::FirstPositionInNode(element));
}
element =
enclosing_text_control_element ? enclosing_text_control_element : element;
Element* parent = element;
if (IsTextControlElement(*element)) {
TextControlElement* text_control = ToTextControlElement(element);
parent = text_control;
element = text_control->InnerEditorElement();
if (!element)
return;
is_text_field = isHTMLInputElement(*text_control) &&
toHTMLInputElement(*text_control).IsTextField();
}
if (is_text_field || !parent->IsAlreadySpellChecked()) {
if (EditingIgnoresContent(*element))
return;
// We always recheck textfields because markers are removed from them on
// blur.
const VisibleSelection selection = CreateVisibleSelection(
SelectionInDOMTree::Builder().SelectAllChildren(*element).Build());
MarkMisspellingsInternal(selection);
if (!is_text_field)
parent->SetAlreadySpellChecked(true);
}
}
void SpellChecker::IgnoreSpelling() {
RemoveMarkers(GetFrame()
.Selection()
.ComputeVisibleSelectionInDOMTree()
.ToNormalizedEphemeralRange(),
DocumentMarker::kSpelling);
}
void SpellChecker::AdvanceToNextMisspelling(bool start_before_selection) {
DocumentLifecycle::DisallowTransitionScope disallow_transition(
GetFrame().GetDocument()->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(
GetFrame().Selection().ComputeVisibleSelectionInDOMTree());
Position spelling_search_start, spelling_search_end;
Range::selectNodeContents(GetFrame().GetDocument(), spelling_search_start,
spelling_search_end);
bool started_with_selection = false;
if (selection.Start().AnchorNode()) {
started_with_selection = true;
if (start_before_selection) {
VisiblePosition start(selection.VisibleStart());
// We match AppKit's rule: Start 1 character before the selection.
VisiblePosition one_before_start = PreviousPositionOf(start);
spelling_search_start =
(one_before_start.IsNotNull() ? one_before_start : start)
.ToParentAnchoredPosition();
} else {
spelling_search_start = selection.VisibleEnd().ToParentAnchoredPosition();
}
}
Position position = spelling_search_start;
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 (!GetFrame().GetDocument()->documentElement())
return;
position = FirstEditableVisiblePositionAfterPositionInRoot(
position, *GetFrame().GetDocument()->documentElement())
.DeepEquivalent();
if (position.IsNull())
return;
spelling_search_start = position.ParentAnchoredEquivalent();
started_with_selection = false; // won't need to wrap
}
// topNode defines the whole range we want to operate on
ContainerNode* top_node = HighestEditableRoot(position);
// TODO(yosin): |lastOffsetForEditing()| is wrong here if
// |editingIgnoresContent(highestEditableRoot())| returns true, e.g. <table>
spelling_search_end = Position::EditingPositionOf(
top_node, EditingStrategy::LastOffsetForEditing(top_node));
// 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 (started_with_selection) {
VisiblePosition one_before_start =
PreviousPositionOf(CreateVisiblePosition(spelling_search_start));
if (one_before_start.IsNotNull() &&
RootEditableElementOf(one_before_start) ==
RootEditableElementOf(spelling_search_start))
spelling_search_start =
EndOfWord(one_before_start).ToParentAnchoredPosition();
// else we were already at the start of the editable node
}
if (spelling_search_start == spelling_search_end)
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* search_end_node_after_wrap = spelling_search_end.ComputeContainerNode();
int search_end_offset_after_wrap =
spelling_search_end.OffsetInContainerNode();
std::pair<String, int> misspelled_item(String(), 0);
String& misspelled_word = misspelled_item.first;
int& misspelling_offset = misspelled_item.second;
misspelled_item =
FindFirstMisspelling(spelling_search_start, spelling_search_end);
// 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 (started_with_selection && !misspelled_word) {
spelling_search_start = Position::EditingPositionOf(top_node, 0);
// going until the end of the very first chunk we tested is far enough
spelling_search_end = Position::EditingPositionOf(
search_end_node_after_wrap, search_end_offset_after_wrap);
misspelled_item =
FindFirstMisspelling(spelling_search_start, spelling_search_end);
}
if (!misspelled_word.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 misspelling_range = CalculateCharacterSubrange(
EphemeralRange(spelling_search_start, spelling_search_end),
misspelling_offset, misspelled_word.length());
GetFrame().Selection().SetSelection(SelectionInDOMTree::Builder()
.SetBaseAndExtent(misspelling_range)
.Build());
GetFrame().Selection().RevealSelection();
GetSpellCheckerClient().UpdateSpellingUIWithMisspelledWord(misspelled_word);
GetFrame().GetDocument()->Markers().AddMarker(
misspelling_range.StartPosition(), misspelling_range.EndPosition(),
DocumentMarker::kSpelling);
}
}
void SpellChecker::ShowSpellingGuessPanel() {
if (GetSpellCheckerClient().SpellingUIIsShowing()) {
GetSpellCheckerClient().ShowSpellingUI(false);
return;
}
AdvanceToNextMisspelling(true);
GetSpellCheckerClient().ShowSpellingUI(true);
}
void SpellChecker::MarkMisspellingsForMovingParagraphs(
const VisibleSelection& moving_selection) {
if (RuntimeEnabledFeatures::idleTimeSpellCheckingEnabled())
return;
// TODO(editing-dev): 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).
GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
DocumentLifecycle::DisallowTransitionScope disallow_transition(
GetFrame().GetDocument()->Lifecycle());
MarkMisspellingsInternal(moving_selection);
}
void SpellChecker::MarkMisspellingsInternal(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* editable_node = range.StartPosition().ComputeContainerNode();
if (!editable_node || !HasEditableStyle(*editable_node))
return;
TextCheckingParagraph full_paragraph_to_check(
ExpandRangeToSentenceBoundary(range));
ChunkAndMarkAllMisspellings(full_paragraph_to_check);
}
void SpellChecker::MarkMisspellingsAfterApplyingCommand(
const CompositeEditCommand& cmd) {
if (RuntimeEnabledFeatures::idleTimeSpellCheckingEnabled())
return;
if (!IsSpellCheckingEnabled())
return;
if (!IsSpellCheckingEnabledFor(cmd.EndingSelection()))
return;
// TODO(editing-dev): 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).
GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
// 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.GetInputType() != InputEvent::InputType::kInsertFromPaste)
return;
MarkMisspellingsAfterReplaceSelectionCommand(ToReplaceSelectionCommand(cmd));
}
void SpellChecker::MarkMisspellingsAfterTypingCommand(
const TypingCommand& cmd) {
spell_check_requester_->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 word_start_of_previous =
StartOfWord(previous, kLeftWordIfOnBoundary);
if (cmd.CommandTypeOfOpenCommand() ==
TypingCommand::kInsertParagraphSeparator) {
VisiblePosition next_word = NextWordPosition(start);
// TODO(yosin): We should make |endOfWord()| not to return null position.
VisibleSelection words = CreateVisibleSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtentDeprecated(word_start_of_previous.DeepEquivalent(),
EndOfWord(next_word).DeepEquivalent())
.SetAffinity(word_start_of_previous.Affinity())
.Build());
MarkMisspellingsAfterLineBreak(words);
return;
}
if (previous.IsNull())
return;
VisiblePosition current_word_start =
StartOfWord(start, kLeftWordIfOnBoundary);
if (word_start_of_previous.DeepEquivalent() ==
current_word_start.DeepEquivalent())
return;
MarkMisspellingsAfterTypingToWord(word_start_of_previous);
}
void SpellChecker::MarkMisspellingsAfterLineBreak(
const VisibleSelection& word_selection) {
TRACE_EVENT0("blink", "SpellChecker::markMisspellingsAfterLineBreak");
MarkMisspellingsInternal(word_selection);
}
void SpellChecker::MarkMisspellingsAfterTypingToWord(
const VisiblePosition& word_start) {
TRACE_EVENT0("blink", "SpellChecker::markMisspellingsAfterTypingToWord");
VisibleSelection adjacent_words =
CreateVisibleSelection(SelectWord(word_start));
MarkMisspellingsInternal(adjacent_words);
}
bool SpellChecker::IsSpellCheckingEnabledInFocusedNode() const {
// To avoid regression on speedometer benchmark[1] test, we should not
// update layout tree in this code block.
// [1] http://browserbench.org/Speedometer/
DocumentLifecycle::DisallowTransitionScope disallow_transition(
GetFrame().GetDocument()->Lifecycle());
Node* focused_node = GetFrame()
.Selection()
.GetSelectionInDOMTree()
.ComputeStartPosition()
.AnchorNode();
if (!focused_node)
return false;
const Element* focused_element = focused_node->IsElementNode()
? ToElement(focused_node)
: focused_node->parentElement();
if (!focused_element)
return false;
return focused_element->IsSpellCheckingEnabled();
}
void SpellChecker::MarkMisspellingsAfterReplaceSelectionCommand(
const ReplaceSelectionCommand& cmd) {
TRACE_EVENT0("blink",
"SpellChecker::markMisspellingsAfterReplaceSelectionCommand");
const EphemeralRange& inserted_range = cmd.InsertedRange();
if (inserted_range.IsNull())
return;
Node* node = cmd.EndingSelection().RootEditableElement();
if (!node)
return;
EphemeralRange paragraph_range(Position::FirstPositionInNode(node),
Position::LastPositionInNode(node));
TextCheckingParagraph text_to_check(inserted_range, paragraph_range);
ChunkAndMarkAllMisspellings(text_to_check);
}
void SpellChecker::ChunkAndMarkAllMisspellings(
const TextCheckingParagraph& full_paragraph_to_check) {
if (full_paragraph_to_check.IsEmpty())
return;
const EphemeralRange& paragraph_range =
full_paragraph_to_check.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 (full_paragraph_to_check.RangeLength() <= kChunkSize) {
spell_check_requester_->RequestCheckingFor(paragraph_range);
return;
}
CharacterIterator check_range_iterator(
full_paragraph_to_check.CheckingRange(),
TextIteratorBehavior::Builder()
.SetEmitsObjectReplacementCharacter(true)
.Build());
for (int request_num = 0; !check_range_iterator.AtEnd(); request_num++) {
EphemeralRange chunk_range =
check_range_iterator.CalculateCharacterSubrange(0, kChunkSize);
EphemeralRange check_range =
request_num ? ExpandEndToSentenceBoundary(chunk_range)
: ExpandRangeToSentenceBoundary(chunk_range);
spell_check_requester_->RequestCheckingFor(check_range, request_num);
if (!check_range_iterator.AtEnd()) {
check_range_iterator.Advance(1);
// The layout should be already update due to the initialization of
// checkRangeIterator, so comparePositions can be directly called.
if (ComparePositions(chunk_range.EndPosition(),
check_range.EndPosition()) < 0)
check_range_iterator.Advance(TextIterator::RangeLength(
chunk_range.EndPosition(), check_range.EndPosition()));
}
}
}
static void AddMarker(Document* document,
const EphemeralRange& checking_range,
DocumentMarker::MarkerType type,
int location,
int length,
const String& description) {
DCHECK_GT(length, 0);
DCHECK_GE(location, 0);
const EphemeralRange& range_to_mark =
CalculateCharacterSubrange(checking_range, location, length);
if (!SpellChecker::IsSpellCheckingEnabledAt(range_to_mark.StartPosition()))
return;
if (!SpellChecker::IsSpellCheckingEnabledAt(range_to_mark.EndPosition()))
return;
document->Markers().AddMarker(range_to_mark.StartPosition(),
range_to_mark.EndPosition(), type, description);
}
void SpellChecker::MarkAndReplaceFor(
SpellCheckRequest* request,
const Vector<TextCheckingResult>& results) {
TRACE_EVENT0("blink", "SpellChecker::markAndReplaceFor");
DCHECK(request);
if (!GetFrame().Selection().IsAvailable()) {
// "editing/spelling/spellcheck-async-remove-frame.html" reaches here.
return;
}
if (!request->IsValid())
return;
if (request->RootEditableElement()->GetDocument() !=
GetFrame().Selection().GetDocument()) {
// we ignore |request| made for another document.
// "editing/spelling/spellcheck-sequencenum.html" and others reach here.
return;
}
// TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
DocumentLifecycle::DisallowTransitionScope disallow_transition(
GetFrame().GetDocument()->Lifecycle());
EphemeralRange checking_range(request->CheckingRange());
// Abort marking if the content of the checking change has been modified.
String current_content =
PlainText(checking_range, TextIteratorBehavior::Builder()
.SetEmitsObjectReplacementCharacter(true)
.Build());
if (current_content != request->Data().GetText()) {
// "editing/spelling/spellcheck-async-mutation.html" reaches here.
return;
}
TextCheckingParagraph paragraph(checking_range, checking_range);
// TODO(xiaochengh): The following comment does not match the current behavior
// and should be rewritten.
// Expand the range to encompass entire paragraphs, since text checking needs
// that much context.
int selection_offset = 0;
int ambiguous_boundary_offset = -1;
if (GetFrame().Selection().ComputeVisibleSelectionInDOMTree().IsCaret()) {
// TODO(xiaochengh): The following comment does not match the current
// behavior and should be rewritten.
// Attempt to save the caret position so we can restore it later if needed
const Position& caret_position =
GetFrame().Selection().ComputeVisibleSelectionInDOMTree().end();
selection_offset = paragraph.OffsetTo(caret_position);
if (selection_offset > 0 &&
static_cast<unsigned>(selection_offset) <=
paragraph.GetText().length() &&
IsAmbiguousBoundaryCharacter(
paragraph.TextCharAt(selection_offset - 1))) {
ambiguous_boundary_offset = selection_offset - 1;
}
}
const int spelling_range_end_offset = paragraph.CheckingEnd();
for (const TextCheckingResult& result : results) {
const int result_location = result.location + paragraph.CheckingStart();
const int result_length = result.length;
const bool result_ends_at_ambiguous_boundary =
ambiguous_boundary_offset >= 0 &&
result_location + result_length == ambiguous_boundary_offset;
// Only mark misspelling if:
// 1. Result falls within spellingRange.
// 2. 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.
switch (result.decoration) {
case kTextDecorationTypeSpelling:
if (result_location < paragraph.CheckingStart() ||
result_location + result_length > spelling_range_end_offset ||
result_ends_at_ambiguous_boundary)
continue;
AddMarker(GetFrame().GetDocument(), paragraph.CheckingRange(),
DocumentMarker::kSpelling, result_location, result_length,
result.replacement);
continue;
case kTextDecorationTypeGrammar:
if (!paragraph.CheckingRangeCovers(result_location, result_length))
continue;
DCHECK_GT(result_length, 0);
DCHECK_GE(result_location, 0);
for (const GrammarDetail& detail : result.details) {
DCHECK_GT(detail.length, 0);
DCHECK_GE(detail.location, 0);
if (!paragraph.CheckingRangeCovers(result_location + detail.location,
detail.length))
continue;
AddMarker(GetFrame().GetDocument(), paragraph.CheckingRange(),
DocumentMarker::kGrammar, result_location + detail.location,
detail.length, result.replacement);
}
continue;
}
NOTREACHED();
}
}
void SpellChecker::UpdateMarkersForWordsAffectedByEditing(
bool do_not_remove_if_selection_at_word_boundary) {
if (RuntimeEnabledFeatures::idleTimeSpellCheckingEnabled())
return;
DCHECK(GetFrame().Selection().IsAvailable());
TRACE_EVENT0("blink", "SpellChecker::updateMarkersForWordsAffectedByEditing");
// TODO(editing-dev): We should hoist
// updateStyleAndLayoutIgnorePendingStylesheets to caller. See
// http://crbug.com/590369 for more details.
GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
if (!IsSpellCheckingEnabledFor(
GetFrame().Selection().ComputeVisibleSelectionInDOMTree()))
return;
Document* document = GetFrame().GetDocument();
DCHECK(document);
// 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 start_of_selection =
GetFrame().Selection().ComputeVisibleSelectionInDOMTree().VisibleStart();
VisiblePosition end_of_selection =
GetFrame().Selection().ComputeVisibleSelectionInDOMTree().VisibleEnd();
if (start_of_selection.IsNull())
return;
// First word is the word that ends after or on the start of selection.
VisiblePosition start_of_first_word =
StartOfWord(start_of_selection, kLeftWordIfOnBoundary);
VisiblePosition end_of_first_word =
EndOfWord(start_of_selection, kLeftWordIfOnBoundary);
// Last word is the word that begins before or on the end of selection
VisiblePosition start_of_last_word =
StartOfWord(end_of_selection, kRightWordIfOnBoundary);
VisiblePosition end_of_last_word =
EndOfWord(end_of_selection, kRightWordIfOnBoundary);
if (start_of_first_word.IsNull()) {
start_of_first_word =
StartOfWord(start_of_selection, kRightWordIfOnBoundary);
end_of_first_word = EndOfWord(start_of_selection, kRightWordIfOnBoundary);
}
if (end_of_last_word.IsNull()) {
start_of_last_word = StartOfWord(end_of_selection, kLeftWordIfOnBoundary);
end_of_last_word = EndOfWord(end_of_selection, kLeftWordIfOnBoundary);
}
// If doNotRemoveIfSelectionAtWordBoundary is true, and first word ends at the
// start of selection, we choose next word as the first word.
if (do_not_remove_if_selection_at_word_boundary &&
end_of_first_word.DeepEquivalent() ==
start_of_selection.DeepEquivalent()) {
start_of_first_word = NextWordPosition(start_of_first_word);
end_of_first_word = EndOfWord(start_of_first_word, kRightWordIfOnBoundary);
if (start_of_first_word.DeepEquivalent() ==
end_of_selection.DeepEquivalent())
return;
}
// If doNotRemoveIfSelectionAtWordBoundary is true, and last word begins at
// the end of selection, we choose previous word as the last word.
if (do_not_remove_if_selection_at_word_boundary &&
start_of_last_word.DeepEquivalent() ==
end_of_selection.DeepEquivalent()) {
start_of_last_word = PreviousWordPosition(start_of_last_word);
end_of_last_word = EndOfWord(start_of_last_word, kRightWordIfOnBoundary);
if (end_of_last_word.DeepEquivalent() ==
start_of_selection.DeepEquivalent())
return;
}
if (start_of_first_word.IsNull() || end_of_first_word.IsNull() ||
start_of_last_word.IsNull() || end_of_last_word.IsNull())
return;
const Position& remove_marker_start = start_of_first_word.DeepEquivalent();
const Position& remove_marker_end = end_of_last_word.DeepEquivalent();
if (remove_marker_start > remove_marker_end) {
// 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 word_range(remove_marker_start, remove_marker_end);
document->Markers().RemoveMarkersInRange(
word_range, DocumentMarker::MisspellingMarkers());
}
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.
if (!RuntimeEnabledFeatures::idleTimeSpellCheckingEnabled())
spell_check_requester_->CancelCheck();
TextControlElement* text_control_element = ToTextControlElement(e);
HTMLElement* inner_editor = text_control_element->InnerEditorElement();
RemoveSpellingAndGrammarMarkers(*inner_editor);
}
void SpellChecker::RemoveSpellingAndGrammarMarkers(const HTMLElement& element,
ElementsType elements_type) {
// TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
GetFrame().GetDocument()->UpdateStyleAndLayoutTreeForNode(&element);
DocumentMarker::MarkerTypes marker_types(DocumentMarker::kSpelling);
marker_types.Add(DocumentMarker::kGrammar);
for (Node& node : NodeTraversal::InclusiveDescendantsOf(element)) {
if (elements_type == ElementsType::kAll || !HasEditableStyle(node))
GetFrame().GetDocument()->Markers().RemoveMarkers(&node, marker_types);
}
}
void SpellChecker::ReplaceMisspelledRange(const String& text) {
EphemeralRange caret_range = GetFrame()
.Selection()
.ComputeVisibleSelectionInDOMTree()
.ToNormalizedEphemeralRange();
if (caret_range.IsNull())
return;
DocumentMarkerVector markers =
GetFrame().GetDocument()->Markers().MarkersInRange(
caret_range, DocumentMarker::MisspellingMarkers());
if (markers.size() < 1 ||
markers[0]->StartOffset() >= markers[0]->EndOffset())
return;
EphemeralRange marker_range = EphemeralRange(
Position(caret_range.StartPosition().ComputeContainerNode(),
markers[0]->StartOffset()),
Position(caret_range.EndPosition().ComputeContainerNode(),
markers[0]->EndOffset()));
if (marker_range.IsNull())
return;
GetFrame().Selection().SetSelection(
SelectionInDOMTree::Builder().SetBaseAndExtent(marker_range).Build());
Document& current_document = *GetFrame().GetDocument();
// Dispatch 'beforeinput'.
Element* const target = GetFrame().GetEditor().FindEventTargetFromSelection();
DataTransfer* const data_transfer = DataTransfer::Create(
DataTransfer::DataTransferType::kInsertReplacementText,
DataTransferAccessPolicy::kDataTransferReadable,
DataObject::CreateFromString(text));
const bool cancel = DispatchBeforeInputDataTransfer(
target, InputEvent::InputType::kInsertReplacementText,
data_transfer) != DispatchEventResult::kNotCanceled;
// 'beforeinput' event handler may destroy target frame.
if (current_document != GetFrame().GetDocument())
return;
// TODO(editing-dev): The use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. See http://crbug.com/590369 for more details.
GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
if (cancel)
return;
GetFrame().GetEditor().ReplaceSelectionWithText(
text, false, false, InputEvent::InputType::kInsertReplacementText);
}
static bool ShouldCheckOldSelection(const Position& old_selection_start) {
if (!old_selection_start.IsConnected())
return false;
if (IsPositionInTextField(old_selection_start))
return false;
if (IsPositionInTextArea(old_selection_start))
return true;
// TODO(editing-dev): 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).
old_selection_start.GetDocument()
->UpdateStyleAndLayoutIgnorePendingStylesheets();
return IsEditablePosition(old_selection_start);
}
void SpellChecker::RespondToChangedSelection(
const Position& old_selection_start,
FrameSelection::SetSelectionOptions options) {
if (RuntimeEnabledFeatures::idleTimeSpellCheckingEnabled()) {
idle_spell_check_callback_->SetNeedsInvocation();
return;
}
TRACE_EVENT0("blink", "SpellChecker::respondToChangedSelection");
if (!IsSpellCheckingEnabledAt(old_selection_start))
return;
// When spell checking is off, existing markers disappear after the selection
// changes.
if (!IsSpellCheckingEnabled()) {
GetFrame().GetDocument()->Markers().RemoveMarkers(
DocumentMarker::kSpelling);
GetFrame().GetDocument()->Markers().RemoveMarkers(DocumentMarker::kGrammar);
return;
}
if (!(options & FrameSelection::kCloseTyping))
return;
if (!ShouldCheckOldSelection(old_selection_start))
return;
// TODO(editing-dev): 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).
GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
DocumentLifecycle::DisallowTransitionScope disallow_transition(
GetFrame().GetDocument()->Lifecycle());
VisibleSelection new_adjacent_words;
const VisibleSelection new_selection =
GetFrame().Selection().ComputeVisibleSelectionInDOMTree();
if (new_selection.IsContentEditable()) {
new_adjacent_words =
CreateVisibleSelection(SelectWord(new_selection.VisibleStart()));
}
// 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(old_selection_start, new_adjacent_words);
}
void SpellChecker::RespondToChangedContents() {
UpdateMarkersForWordsAffectedByEditing(true);
if (RuntimeEnabledFeatures::idleTimeSpellCheckingEnabled())
idle_spell_check_callback_->SetNeedsInvocation();
}
void SpellChecker::RemoveSpellingMarkers() {
GetFrame().GetDocument()->Markers().RemoveMarkers(
DocumentMarker::MisspellingMarkers());
}
void SpellChecker::RemoveSpellingMarkersUnderWords(
const Vector<String>& words) {
MarkerRemoverPredicate remover_predicate(words);
DocumentMarkerController& marker_controller =
GetFrame().GetDocument()->Markers();
marker_controller.RemoveMarkers(remover_predicate);
marker_controller.RepaintMarkers();
}
void SpellChecker::SpellCheckAfterBlur() {
if (RuntimeEnabledFeatures::idleTimeSpellCheckingEnabled())
return;
// TODO(editing-dev): Hoist updateStyleAndLayoutIgnorePendingStylesheets
// to caller. 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).
GetFrame().GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
DocumentLifecycle::DisallowTransitionScope disallow_transition(
GetFrame().GetDocument()->Lifecycle());
if (!GetFrame()
.Selection()
.ComputeVisibleSelectionInDOMTree()
.IsContentEditable())
return;
if (IsPositionInTextField(
GetFrame().Selection().ComputeVisibleSelectionInDOMTree().Start())) {
// textFieldDidEndEditing() and textFieldDidBeginEditing() handle this.
return;
}
VisibleSelection empty;
SpellCheckOldSelection(
GetFrame().Selection().ComputeVisibleSelectionInDOMTree().Start(), empty);
}
void SpellChecker::SpellCheckOldSelection(
const Position& old_selection_start,
const VisibleSelection& new_adjacent_words) {
if (!IsSpellCheckingEnabled())
return;
TRACE_EVENT0("blink", "SpellChecker::spellCheckOldSelection");
VisiblePosition old_start = CreateVisiblePosition(old_selection_start);
VisibleSelection old_adjacent_words =
CreateVisibleSelection(SelectWord(old_start));
if (old_adjacent_words == new_adjacent_words)
return;
MarkMisspellingsInternal(old_adjacent_words);
}
static Node* FindFirstMarkable(Node* node) {
while (node) {
if (!node->GetLayoutObject())
return 0;
if (node->GetLayoutObject()->IsText())
return node;
if (node->GetLayoutObject()->IsTextControl())
node = ToLayoutTextControl(node->GetLayoutObject())
->GetTextControlElement()
->VisiblePositionForIndex(1)
.DeepEquivalent()
.AnchorNode();
else if (node->hasChildren())
node = node->firstChild();
else
node = node->nextSibling();
}
return 0;
}
bool SpellChecker::SelectionStartHasMarkerFor(
DocumentMarker::MarkerType marker_type,
int from,
int length) const {
Node* node = FindFirstMarkable(GetFrame()
.Selection()
.ComputeVisibleSelectionInDOMTree()
.Start()
.AnchorNode());
if (!node)
return false;
unsigned start_offset = static_cast<unsigned>(from);
unsigned end_offset = static_cast<unsigned>(from + length);
DocumentMarkerVector markers =
GetFrame().GetDocument()->Markers().MarkersFor(node);
for (size_t i = 0; i < markers.size(); ++i) {
DocumentMarker* marker = markers[i];
if (marker->StartOffset() <= start_offset &&
end_offset <= marker->EndOffset() && marker->GetType() == marker_type)
return true;
}
return false;
}
void SpellChecker::RemoveMarkers(const EphemeralRange& range,
DocumentMarker::MarkerTypes marker_types) {
DCHECK(!GetFrame().GetDocument()->NeedsLayoutTreeUpdate());
if (range.IsNull())
return;
GetFrame().GetDocument()->Markers().RemoveMarkersInRange(range, marker_types);
}
// TODO(xiaochengh): This function is only used by unit tests. We should move it
// to IdleSpellCheckCallback and modify unit tests to cope with idle time spell
// checker.
void SpellChecker::CancelCheck() {
spell_check_requester_->CancelCheck();
}
void SpellChecker::DocumentAttached(Document* document) {
if (RuntimeEnabledFeatures::idleTimeSpellCheckingEnabled())
idle_spell_check_callback_->DocumentAttached(document);
}
DEFINE_TRACE(SpellChecker) {
visitor->Trace(frame_);
visitor->Trace(spell_check_requester_);
visitor->Trace(idle_spell_check_callback_);
}
void SpellChecker::PrepareForLeakDetection() {
spell_check_requester_->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 word_start = iterator->current();
while (word_start >= 0) {
int word_end = iterator->next();
if (word_end < 0)
break;
int word_length = word_end - word_start;
int misspelling_location = -1;
int misspelling_length = 0;
TextChecker().CheckSpellingOfString(
String(characters.data() + word_start, word_length),
&misspelling_location, &misspelling_length);
if (misspelling_length > 0) {
DCHECK_GE(misspelling_location, 0);
DCHECK_LE(misspelling_location + misspelling_length, word_length);
TextCheckingResult misspelling;
misspelling.decoration = kTextDecorationTypeSpelling;
misspelling.location = word_start + misspelling_location;
misspelling.length = misspelling_length;
results.push_back(misspelling);
}
word_start = word_end;
}
return results;
}
std::pair<String, int> SpellChecker::FindFirstMisspelling(const Position& start,
const Position& end) {
String misspelled_word;
// Initialize out parameters; they will be updated if we find something to
// return.
String first_found_item;
int first_found_offset = 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 paragraph_start =
StartOfParagraph(CreateVisiblePosition(start)).ToParentAnchoredPosition();
Position paragraph_end = end;
int total_range_length =
TextIterator::RangeLength(paragraph_start, paragraph_end);
paragraph_end =
EndOfParagraph(CreateVisiblePosition(start)).ToParentAnchoredPosition();
int range_start_offset = TextIterator::RangeLength(paragraph_start, start);
int total_length_processed = 0;
bool first_iteration = true;
bool last_iteration = false;
while (total_length_processed < total_range_length) {
// Iterate through the search range by paragraphs, checking each one for
// spelling.
int current_length =
TextIterator::RangeLength(paragraph_start, paragraph_end);
int current_start_offset = first_iteration ? range_start_offset : 0;
int current_end_offset = current_length;
if (InSameParagraph(CreateVisiblePosition(paragraph_start),
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.
current_end_offset = TextIterator::RangeLength(paragraph_start, end);
last_iteration = true;
}
if (current_start_offset < current_end_offset) {
String paragraph_string =
PlainText(EphemeralRange(paragraph_start, paragraph_end));
if (paragraph_string.length() > 0) {
int spelling_location = 0;
Vector<TextCheckingResult> results = FindMisspellings(paragraph_string);
for (unsigned i = 0; i < results.size(); i++) {
const TextCheckingResult* result = &results[i];
if (result->location >= current_start_offset &&
result->location + result->length <= current_end_offset) {
DCHECK_GT(result->length, 0);
DCHECK_GE(result->location, 0);
spelling_location = result->location;
misspelled_word =
paragraph_string.Substring(result->location, result->length);
DCHECK(misspelled_word.length());
break;
}
}
if (!misspelled_word.IsEmpty()) {
int spelling_offset = spelling_location - current_start_offset;
if (!first_iteration)
spelling_offset +=
TextIterator::RangeLength(start, paragraph_start);
first_found_offset = spelling_offset;
first_found_item = misspelled_word;
break;
}
}
}
if (last_iteration ||
total_length_processed + current_length >= total_range_length)
break;
VisiblePosition new_paragraph_start =
StartOfNextParagraph(CreateVisiblePosition(paragraph_end));
if (new_paragraph_start.IsNull())
break;
paragraph_start = new_paragraph_start.ToParentAnchoredPosition();
paragraph_end =
EndOfParagraph(new_paragraph_start).ToParentAnchoredPosition();
first_iteration = false;
total_length_processed += current_length;
}
return std::make_pair(first_found_item, first_found_offset);
}
// static
bool SpellChecker::IsSpellCheckingEnabledAt(const Position& position) {
if (position.IsNull())
return false;
if (TextControlElement* text_control = EnclosingTextControl(position)) {
if (isHTMLInputElement(text_control)) {
HTMLInputElement& input = toHTMLInputElement(*text_control);
// TODO(tkent): The following password type check should be done in
// HTMLElement::spellcheck(). crbug.com/371567
if (input.type() == InputTypeNames::password)
return false;
if (!input.IsFocusedElementInDocument())
return false;
}
}
HTMLElement* element =
Traversal<HTMLElement>::FirstAncestorOrSelf(*position.AnchorNode());
return element && element->IsSpellCheckingEnabled();
}
} // namespace blink