flutter_flutter/engine/core/editing/TextCheckingHelper.cpp
Eric Seidel e0fd75b5ab Make absolute and sort all Sky headers
This caused us to lose our gn check certification. :(

Turns out gn check was just ignoring all the header
paths it didn't understand and so gn check passing
for sky wasn't meaning much.  I tried to straighten
out some of the mess in this CL, but its going to take
several more rounds of massaging before gn check
passes again.  On the bright side (almost) all of
our headers are absolute now.  Turns out my script
(attached to the bug) didn't notice ../ includes
but I'll fix that in the next patch.

R=abarth@chromium.org
BUG=435361

Review URL: https://codereview.chromium.org/746023002
2014-11-20 17:42:05 -08:00

579 lines
25 KiB
C++

/*
* Copyright (C) 2006, 2007 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 "sky/engine/config.h"
#include "sky/engine/core/editing/TextCheckingHelper.h"
#include "sky/engine/bindings/core/v8/ExceptionState.h"
#include "sky/engine/bindings/core/v8/ExceptionStatePlaceholder.h"
#include "sky/engine/core/dom/Document.h"
#include "sky/engine/core/dom/DocumentMarkerController.h"
#include "sky/engine/core/dom/Range.h"
#include "sky/engine/core/editing/TextIterator.h"
#include "sky/engine/core/editing/VisiblePosition.h"
#include "sky/engine/core/editing/VisibleUnits.h"
#include "sky/engine/core/frame/LocalFrame.h"
#include "sky/engine/core/frame/Settings.h"
#include "sky/engine/core/page/SpellCheckerClient.h"
#include "sky/engine/platform/text/TextBreakIterator.h"
#include "sky/engine/platform/text/TextCheckerClient.h"
namespace blink {
static void findBadGrammars(TextCheckerClient& client, const UChar* text, int start, int length, Vector<TextCheckingResult>& results)
{
int checkLocation = start;
int checkLength = length;
while (0 < checkLength) {
int badGrammarLocation = -1;
int badGrammarLength = 0;
Vector<GrammarDetail> badGrammarDetails;
client.checkGrammarOfString(String(text + checkLocation, checkLength), badGrammarDetails, &badGrammarLocation, &badGrammarLength);
if (!badGrammarLength)
break;
ASSERT(0 <= badGrammarLocation && badGrammarLocation <= checkLength);
ASSERT(0 < badGrammarLength && badGrammarLocation + badGrammarLength <= checkLength);
TextCheckingResult badGrammar;
badGrammar.decoration = TextDecorationTypeGrammar;
badGrammar.location = checkLocation + badGrammarLocation;
badGrammar.length = badGrammarLength;
badGrammar.details.swap(badGrammarDetails);
results.append(badGrammar);
checkLocation += (badGrammarLocation + badGrammarLength);
checkLength -= (badGrammarLocation + badGrammarLength);
}
}
static void findMisspellings(TextCheckerClient& client, const UChar* text, int start, int length, Vector<TextCheckingResult>& results)
{
TextBreakIterator* iterator = wordBreakIterator(text + start, length);
if (!iterator)
return;
int wordStart = iterator->current();
while (0 <= wordStart) {
int wordEnd = iterator->next();
if (wordEnd < 0)
break;
int wordLength = wordEnd - wordStart;
int misspellingLocation = -1;
int misspellingLength = 0;
client.checkSpellingOfString(String(text + start + wordStart, wordLength), &misspellingLocation, &misspellingLength);
if (0 < misspellingLength) {
ASSERT(0 <= misspellingLocation && misspellingLocation <= wordLength);
ASSERT(0 < misspellingLength && misspellingLocation + misspellingLength <= wordLength);
TextCheckingResult misspelling;
misspelling.decoration = TextDecorationTypeSpelling;
misspelling.location = start + wordStart + misspellingLocation;
misspelling.length = misspellingLength;
misspelling.replacement = client.getAutoCorrectSuggestionForMisspelledWord(String(text + misspelling.location, misspelling.length));
results.append(misspelling);
}
wordStart = wordEnd;
}
}
static PassRefPtr<Range> expandToParagraphBoundary(PassRefPtr<Range> range)
{
RefPtr<Range> paragraphRange = range->cloneRange();
setStart(paragraphRange.get(), startOfParagraph(VisiblePosition(range->startPosition())));
setEnd(paragraphRange.get(), endOfParagraph(VisiblePosition(range->endPosition())));
return paragraphRange;
}
TextCheckingParagraph::TextCheckingParagraph(PassRefPtr<Range> checkingRange)
: m_checkingRange(checkingRange)
, m_checkingStart(-1)
, m_checkingEnd(-1)
, m_checkingLength(-1)
{
}
TextCheckingParagraph::TextCheckingParagraph(PassRefPtr<Range> checkingRange, PassRefPtr<Range> paragraphRange)
: m_checkingRange(checkingRange)
, m_paragraphRange(paragraphRange)
, m_checkingStart(-1)
, m_checkingEnd(-1)
, m_checkingLength(-1)
{
}
TextCheckingParagraph::~TextCheckingParagraph()
{
}
void TextCheckingParagraph::expandRangeToNextEnd()
{
ASSERT(m_checkingRange);
setEnd(paragraphRange().get(), endOfParagraph(startOfNextParagraph(VisiblePosition(paragraphRange()->startPosition()))));
invalidateParagraphRangeValues();
}
void TextCheckingParagraph::invalidateParagraphRangeValues()
{
m_checkingStart = m_checkingEnd = -1;
m_offsetAsRange = nullptr;
m_text = String();
}
int TextCheckingParagraph::rangeLength() const
{
ASSERT(m_checkingRange);
return TextIterator::rangeLength(paragraphRange().get());
}
PassRefPtr<Range> TextCheckingParagraph::paragraphRange() const
{
ASSERT(m_checkingRange);
if (!m_paragraphRange)
m_paragraphRange = expandToParagraphBoundary(checkingRange());
return m_paragraphRange;
}
PassRefPtr<Range> TextCheckingParagraph::subrange(int characterOffset, int characterCount) const
{
ASSERT(m_checkingRange);
return TextIterator::subrange(paragraphRange().get(), characterOffset, characterCount);
}
int TextCheckingParagraph::offsetTo(const Position& position, ExceptionState& exceptionState) const
{
ASSERT(m_checkingRange);
RefPtr<Range> range = offsetAsRange()->cloneRange();
range->setEnd(position.containerNode(), position.computeOffsetInContainerNode(), exceptionState);
if (exceptionState.hadException())
return 0;
return TextIterator::rangeLength(range.get());
}
bool TextCheckingParagraph::isEmpty() const
{
// Both predicates should have same result, but we check both just for sure.
// We need to investigate to remove this redundancy.
return isRangeEmpty() || isTextEmpty();
}
PassRefPtr<Range> TextCheckingParagraph::offsetAsRange() const
{
ASSERT(m_checkingRange);
if (!m_offsetAsRange)
m_offsetAsRange = Range::create(paragraphRange()->startContainer()->document(), paragraphRange()->startPosition(), checkingRange()->startPosition());
return m_offsetAsRange;
}
const String& TextCheckingParagraph::text() const
{
ASSERT(m_checkingRange);
if (m_text.isEmpty())
m_text = plainText(paragraphRange().get());
return m_text;
}
int TextCheckingParagraph::checkingStart() const
{
ASSERT(m_checkingRange);
if (m_checkingStart == -1)
m_checkingStart = TextIterator::rangeLength(offsetAsRange().get());
return m_checkingStart;
}
int TextCheckingParagraph::checkingEnd() const
{
ASSERT(m_checkingRange);
if (m_checkingEnd == -1)
m_checkingEnd = checkingStart() + TextIterator::rangeLength(checkingRange().get());
return m_checkingEnd;
}
int TextCheckingParagraph::checkingLength() const
{
ASSERT(m_checkingRange);
if (-1 == m_checkingLength)
m_checkingLength = TextIterator::rangeLength(checkingRange().get());
return m_checkingLength;
}
TextCheckingHelper::TextCheckingHelper(SpellCheckerClient& client, PassRefPtr<Range> range)
: m_client(&client)
, m_range(range)
{
ASSERT_ARG(m_range, m_range);
}
TextCheckingHelper::~TextCheckingHelper()
{
}
String TextCheckingHelper::findFirstMisspelling(int& firstMisspellingOffset, bool markAll, RefPtr<Range>& firstMisspellingRange)
{
WordAwareIterator it(m_range.get());
firstMisspellingOffset = 0;
String firstMisspelling;
int currentChunkOffset = 0;
while (!it.atEnd()) {
int length = it.length();
// Skip some work for one-space-char hunks
if (!(length == 1 && it.characterAt(0) == ' ')) {
int misspellingLocation = -1;
int misspellingLength = 0;
m_client->textChecker().checkSpellingOfString(it.substring(0, length), &misspellingLocation, &misspellingLength);
// 5490627 shows that there was some code path here where the String constructor below crashes.
// We don't know exactly what combination of bad input caused this, so we're making this much
// more robust against bad input on release builds.
ASSERT(misspellingLength >= 0);
ASSERT(misspellingLocation >= -1);
ASSERT(!misspellingLength || misspellingLocation >= 0);
ASSERT(misspellingLocation < length);
ASSERT(misspellingLength <= length);
ASSERT(misspellingLocation + misspellingLength <= length);
if (misspellingLocation >= 0 && misspellingLength > 0 && misspellingLocation < length && misspellingLength <= length && misspellingLocation + misspellingLength <= length) {
// Compute range of misspelled word
RefPtr<Range> misspellingRange = TextIterator::subrange(m_range.get(), currentChunkOffset + misspellingLocation, misspellingLength);
// Remember first-encountered misspelling and its offset.
if (!firstMisspelling) {
firstMisspellingOffset = currentChunkOffset + misspellingLocation;
firstMisspelling = it.substring(misspellingLocation, misspellingLength);
firstMisspellingRange = misspellingRange;
}
// Store marker for misspelled word.
misspellingRange->startContainer()->document().markers().addMarker(misspellingRange.get(), DocumentMarker::Spelling);
// Bail out if we're marking only the first misspelling, and not all instances.
if (!markAll)
break;
}
}
currentChunkOffset += length;
it.advance();
}
return firstMisspelling;
}
String TextCheckingHelper::findFirstMisspellingOrBadGrammar(bool checkGrammar, bool& outIsSpelling, int& outFirstFoundOffset, GrammarDetail& outGrammarDetail)
{
if (!unifiedTextCheckerEnabled())
return "";
String firstFoundItem;
String misspelledWord;
String badGrammarPhrase;
// Initialize out parameters; these will be updated if we find something to return.
outIsSpelling = true;
outFirstFoundOffset = 0;
outGrammarDetail.location = -1;
outGrammarDetail.length = 0;
outGrammarDetail.guesses.clear();
outGrammarDetail.userDescription = "";
// Expand the search range to encompass entire paragraphs, since text checking needs that much context.
// Determine the character offset from the start of the paragraph to the start of the original search range,
// since we will want to ignore results in this area.
RefPtr<Range> paragraphRange = m_range->cloneRange();
setStart(paragraphRange.get(), startOfParagraph(VisiblePosition(m_range->startPosition())));
int totalRangeLength = TextIterator::rangeLength(paragraphRange.get());
setEnd(paragraphRange.get(), endOfParagraph(VisiblePosition(m_range->startPosition())));
RefPtr<Range> offsetAsRange = Range::create(paragraphRange->startContainer()->document(), paragraphRange->startPosition(), m_range->startPosition());
int rangeStartOffset = TextIterator::rangeLength(offsetAsRange.get());
int totalLengthProcessed = 0;
bool firstIteration = true;
bool lastIteration = false;
while (totalLengthProcessed < totalRangeLength) {
// Iterate through the search range by paragraphs, checking each one for spelling and grammar.
int currentLength = TextIterator::rangeLength(paragraphRange.get());
int currentStartOffset = firstIteration ? rangeStartOffset : 0;
int currentEndOffset = currentLength;
if (inSameParagraph(VisiblePosition(paragraphRange->startPosition()), VisiblePosition(m_range->endPosition()))) {
// Determine the character offset from the end of the original search range to the end of the paragraph,
// since we will want to ignore results in this area.
RefPtr<Range> endOffsetAsRange = Range::create(paragraphRange->startContainer()->document(), paragraphRange->startPosition(), m_range->endPosition());
currentEndOffset = TextIterator::rangeLength(endOffsetAsRange.get());
lastIteration = true;
}
if (currentStartOffset < currentEndOffset) {
String paragraphString = plainText(paragraphRange.get());
if (paragraphString.length() > 0) {
bool foundGrammar = false;
int spellingLocation = 0;
int grammarPhraseLocation = 0;
int grammarDetailLocation = 0;
unsigned grammarDetailIndex = 0;
Vector<TextCheckingResult> results;
TextCheckingTypeMask checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling;
checkTextOfParagraph(m_client->textChecker(), paragraphString, checkingTypes, results);
for (unsigned i = 0; i < results.size(); i++) {
const TextCheckingResult* result = &results[i];
if (result->decoration == TextDecorationTypeSpelling && result->location >= currentStartOffset && result->location + result->length <= currentEndOffset) {
ASSERT(result->length > 0 && result->location >= 0);
spellingLocation = result->location;
misspelledWord = paragraphString.substring(result->location, result->length);
ASSERT(misspelledWord.length());
break;
}
if (checkGrammar && result->decoration == TextDecorationTypeGrammar && result->location < currentEndOffset && result->location + result->length > currentStartOffset) {
ASSERT(result->length > 0 && result->location >= 0);
// We can't stop after the first grammar result, since there might still be a spelling result after
// it begins but before the first detail in it, but we can stop if we find a second grammar result.
if (foundGrammar)
break;
for (unsigned j = 0; j < result->details.size(); j++) {
const GrammarDetail* detail = &result->details[j];
ASSERT(detail->length > 0 && detail->location >= 0);
if (result->location + detail->location >= currentStartOffset && result->location + detail->location + detail->length <= currentEndOffset && (!foundGrammar || result->location + detail->location < grammarDetailLocation)) {
grammarDetailIndex = j;
grammarDetailLocation = result->location + detail->location;
foundGrammar = true;
}
}
if (foundGrammar) {
grammarPhraseLocation = result->location;
outGrammarDetail = result->details[grammarDetailIndex];
badGrammarPhrase = paragraphString.substring(result->location, result->length);
ASSERT(badGrammarPhrase.length());
}
}
}
if (!misspelledWord.isEmpty() && (!checkGrammar || badGrammarPhrase.isEmpty() || spellingLocation <= grammarDetailLocation)) {
int spellingOffset = spellingLocation - currentStartOffset;
if (!firstIteration) {
RefPtr<Range> paragraphOffsetAsRange = Range::create(paragraphRange->startContainer()->document(), m_range->startPosition(), paragraphRange->startPosition());
spellingOffset += TextIterator::rangeLength(paragraphOffsetAsRange.get());
}
outIsSpelling = true;
outFirstFoundOffset = spellingOffset;
firstFoundItem = misspelledWord;
break;
}
if (checkGrammar && !badGrammarPhrase.isEmpty()) {
int grammarPhraseOffset = grammarPhraseLocation - currentStartOffset;
if (!firstIteration) {
RefPtr<Range> paragraphOffsetAsRange = Range::create(paragraphRange->startContainer()->document(), m_range->startPosition(), paragraphRange->startPosition());
grammarPhraseOffset += TextIterator::rangeLength(paragraphOffsetAsRange.get());
}
outIsSpelling = false;
outFirstFoundOffset = grammarPhraseOffset;
firstFoundItem = badGrammarPhrase;
break;
}
}
}
if (lastIteration || totalLengthProcessed + currentLength >= totalRangeLength)
break;
VisiblePosition newParagraphStart = startOfNextParagraph(VisiblePosition(paragraphRange->endPosition()));
setStart(paragraphRange.get(), newParagraphStart);
setEnd(paragraphRange.get(), endOfParagraph(newParagraphStart));
firstIteration = false;
totalLengthProcessed += currentLength;
}
return firstFoundItem;
}
int TextCheckingHelper::findFirstGrammarDetail(const Vector<GrammarDetail>& grammarDetails, int badGrammarPhraseLocation, int startOffset, int endOffset, bool markAll) const
{
// Found some bad grammar. Find the earliest detail range that starts in our search range (if any).
// Optionally add a DocumentMarker for each detail in the range.
int earliestDetailLocationSoFar = -1;
int earliestDetailIndex = -1;
for (unsigned i = 0; i < grammarDetails.size(); i++) {
const GrammarDetail* detail = &grammarDetails[i];
ASSERT(detail->length > 0 && detail->location >= 0);
int detailStartOffsetInParagraph = badGrammarPhraseLocation + detail->location;
// Skip this detail if it starts before the original search range
if (detailStartOffsetInParagraph < startOffset)
continue;
// Skip this detail if it starts after the original search range
if (detailStartOffsetInParagraph >= endOffset)
continue;
if (markAll) {
RefPtr<Range> badGrammarRange = TextIterator::subrange(m_range.get(), badGrammarPhraseLocation - startOffset + detail->location, detail->length);
badGrammarRange->startContainer()->document().markers().addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription);
}
// Remember this detail only if it's earlier than our current candidate (the details aren't in a guaranteed order)
if (earliestDetailIndex < 0 || earliestDetailLocationSoFar > detail->location) {
earliestDetailIndex = i;
earliestDetailLocationSoFar = detail->location;
}
}
return earliestDetailIndex;
}
String TextCheckingHelper::findFirstBadGrammar(GrammarDetail& outGrammarDetail, int& outGrammarPhraseOffset, bool markAll)
{
// Initialize out parameters; these will be updated if we find something to return.
outGrammarDetail.location = -1;
outGrammarDetail.length = 0;
outGrammarDetail.guesses.clear();
outGrammarDetail.userDescription = "";
outGrammarPhraseOffset = 0;
String firstBadGrammarPhrase;
// Expand the search range to encompass entire paragraphs, since grammar checking needs that much context.
// Determine the character offset from the start of the paragraph to the start of the original search range,
// since we will want to ignore results in this area.
TextCheckingParagraph paragraph(m_range);
// Start checking from beginning of paragraph, but skip past results that occur before the start of the original search range.
int startOffset = 0;
while (startOffset < paragraph.checkingEnd()) {
Vector<GrammarDetail> grammarDetails;
int badGrammarPhraseLocation = -1;
int badGrammarPhraseLength = 0;
m_client->textChecker().checkGrammarOfString(paragraph.textSubstring(startOffset), grammarDetails, &badGrammarPhraseLocation, &badGrammarPhraseLength);
if (!badGrammarPhraseLength) {
ASSERT(badGrammarPhraseLocation == -1);
return String();
}
ASSERT(badGrammarPhraseLocation >= 0);
badGrammarPhraseLocation += startOffset;
// Found some bad grammar. Find the earliest detail range that starts in our search range (if any).
int badGrammarIndex = findFirstGrammarDetail(grammarDetails, badGrammarPhraseLocation, paragraph.checkingStart(), paragraph.checkingEnd(), markAll);
if (badGrammarIndex >= 0) {
ASSERT(static_cast<unsigned>(badGrammarIndex) < grammarDetails.size());
outGrammarDetail = grammarDetails[badGrammarIndex];
}
// If we found a detail in range, then we have found the first bad phrase (unless we found one earlier but
// kept going so we could mark all instances).
if (badGrammarIndex >= 0 && firstBadGrammarPhrase.isEmpty()) {
outGrammarPhraseOffset = badGrammarPhraseLocation - paragraph.checkingStart();
firstBadGrammarPhrase = paragraph.textSubstring(badGrammarPhraseLocation, badGrammarPhraseLength);
// Found one. We're done now, unless we're marking each instance.
if (!markAll)
break;
}
// These results were all between the start of the paragraph and the start of the search range; look
// beyond this phrase.
startOffset = badGrammarPhraseLocation + badGrammarPhraseLength;
}
return firstBadGrammarPhrase;
}
void TextCheckingHelper::markAllMisspellings(RefPtr<Range>& firstMisspellingRange)
{
// Use the "markAll" feature of findFirstMisspelling. Ignore the return value and the "out parameter";
// all we need to do is mark every instance.
int ignoredOffset;
findFirstMisspelling(ignoredOffset, true, firstMisspellingRange);
}
void TextCheckingHelper::markAllBadGrammar()
{
// Use the "markAll" feature of ofindFirstBadGrammar. Ignore the return value and "out parameters"; all we need to
// do is mark every instance.
GrammarDetail ignoredGrammarDetail;
int ignoredOffset;
findFirstBadGrammar(ignoredGrammarDetail, ignoredOffset, true);
}
bool TextCheckingHelper::unifiedTextCheckerEnabled() const
{
if (!m_range)
return false;
Document& doc = m_range->ownerDocument();
return blink::unifiedTextCheckerEnabled(doc.frame());
}
void checkTextOfParagraph(TextCheckerClient& client, const String& text, TextCheckingTypeMask checkingTypes, Vector<TextCheckingResult>& results)
{
Vector<UChar> characters;
text.appendTo(characters);
unsigned length = text.length();
Vector<TextCheckingResult> spellingResult;
if (checkingTypes & TextCheckingTypeSpelling)
findMisspellings(client, characters.data(), 0, length, spellingResult);
Vector<TextCheckingResult> grammarResult;
if (checkingTypes & TextCheckingTypeGrammar) {
// Only checks grammartical error before the first misspellings
int grammarCheckLength = length;
for (size_t i = 0; i < spellingResult.size(); ++i) {
if (spellingResult[i].location < grammarCheckLength)
grammarCheckLength = spellingResult[i].location;
}
findBadGrammars(client, characters.data(), 0, grammarCheckLength, grammarResult);
}
if (grammarResult.size())
results.swap(grammarResult);
if (spellingResult.size()) {
if (results.isEmpty())
results.swap(spellingResult);
else
results.appendVector(spellingResult);
}
}
bool unifiedTextCheckerEnabled(const LocalFrame* frame)
{
if (!frame)
return false;
const Settings* settings = frame->settings();
if (!settings)
return false;
return settings->unifiedTextCheckerEnabled();
}
}