2 * Copyright (C) 2006, 2007 Apple Inc. All rights reserved.
3 * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
14 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
18 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
22 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 #include "core/editing/TextCheckingHelper.h"
30 #include "bindings/v8/ExceptionState.h"
31 #include "bindings/v8/ExceptionStatePlaceholder.h"
32 #include "core/dom/Document.h"
33 #include "core/dom/DocumentMarkerController.h"
34 #include "core/dom/Range.h"
35 #include "core/editing/TextIterator.h"
36 #include "core/editing/VisiblePosition.h"
37 #include "core/editing/VisibleUnits.h"
38 #include "core/frame/Frame.h"
39 #include "core/page/Settings.h"
40 #include "platform/text/TextBreakIterator.h"
41 #include "platform/text/TextCheckerClient.h"
45 static void findBadGrammars(TextCheckerClient& client, const UChar* text, int start, int length, Vector<TextCheckingResult>& results)
47 int checkLocation = start;
48 int checkLength = length;
50 while (0 < checkLength) {
51 int badGrammarLocation = -1;
52 int badGrammarLength = 0;
53 Vector<GrammarDetail> badGrammarDetails;
54 client.checkGrammarOfString(String(text + checkLocation, checkLength), badGrammarDetails, &badGrammarLocation, &badGrammarLength);
55 if (!badGrammarLength)
57 ASSERT(0 <= badGrammarLocation && badGrammarLocation <= checkLength);
58 ASSERT(0 < badGrammarLength && badGrammarLocation + badGrammarLength <= checkLength);
59 TextCheckingResult badGrammar;
60 badGrammar.decoration = TextDecorationTypeGrammar;
61 badGrammar.location = checkLocation + badGrammarLocation;
62 badGrammar.length = badGrammarLength;
63 badGrammar.details.swap(badGrammarDetails);
64 results.append(badGrammar);
66 checkLocation += (badGrammarLocation + badGrammarLength);
67 checkLength -= (badGrammarLocation + badGrammarLength);
71 static void findMisspellings(TextCheckerClient& client, const UChar* text, int start, int length, Vector<TextCheckingResult>& results)
73 TextBreakIterator* iterator = wordBreakIterator(text + start, length);
76 int wordStart = iterator->current();
77 while (0 <= wordStart) {
78 int wordEnd = iterator->next();
81 int wordLength = wordEnd - wordStart;
82 int misspellingLocation = -1;
83 int misspellingLength = 0;
84 client.checkSpellingOfString(String(text + start + wordStart, wordLength), &misspellingLocation, &misspellingLength);
85 if (0 < misspellingLength) {
86 ASSERT(0 <= misspellingLocation && misspellingLocation <= wordLength);
87 ASSERT(0 < misspellingLength && misspellingLocation + misspellingLength <= wordLength);
88 TextCheckingResult misspelling;
89 misspelling.decoration = TextDecorationTypeSpelling;
90 misspelling.location = start + wordStart + misspellingLocation;
91 misspelling.length = misspellingLength;
92 misspelling.replacement = client.getAutoCorrectSuggestionForMisspelledWord(String(text + misspelling.location, misspelling.length));
93 results.append(misspelling);
100 static PassRefPtr<Range> expandToParagraphBoundary(PassRefPtr<Range> range)
102 RefPtr<Range> paragraphRange = range->cloneRange(IGNORE_EXCEPTION);
103 setStart(paragraphRange.get(), startOfParagraph(range->startPosition()));
104 setEnd(paragraphRange.get(), endOfParagraph(range->endPosition()));
105 return paragraphRange;
108 TextCheckingParagraph::TextCheckingParagraph(PassRefPtr<Range> checkingRange)
109 : m_checkingRange(checkingRange)
110 , m_checkingStart(-1)
112 , m_checkingLength(-1)
116 TextCheckingParagraph::TextCheckingParagraph(PassRefPtr<Range> checkingRange, PassRefPtr<Range> paragraphRange)
117 : m_checkingRange(checkingRange)
118 , m_paragraphRange(paragraphRange)
119 , m_checkingStart(-1)
121 , m_checkingLength(-1)
125 TextCheckingParagraph::~TextCheckingParagraph()
129 void TextCheckingParagraph::expandRangeToNextEnd()
131 ASSERT(m_checkingRange);
132 setEnd(paragraphRange().get(), endOfParagraph(startOfNextParagraph(paragraphRange()->startPosition())));
133 invalidateParagraphRangeValues();
136 void TextCheckingParagraph::invalidateParagraphRangeValues()
138 m_checkingStart = m_checkingEnd = -1;
143 int TextCheckingParagraph::rangeLength() const
145 ASSERT(m_checkingRange);
146 return TextIterator::rangeLength(paragraphRange().get());
149 PassRefPtr<Range> TextCheckingParagraph::paragraphRange() const
151 ASSERT(m_checkingRange);
152 if (!m_paragraphRange)
153 m_paragraphRange = expandToParagraphBoundary(checkingRange());
154 return m_paragraphRange;
157 PassRefPtr<Range> TextCheckingParagraph::subrange(int characterOffset, int characterCount) const
159 ASSERT(m_checkingRange);
160 return TextIterator::subrange(paragraphRange().get(), characterOffset, characterCount);
163 int TextCheckingParagraph::offsetTo(const Position& position, ExceptionState& es) const
165 ASSERT(m_checkingRange);
166 RefPtr<Range> range = offsetAsRange()->cloneRange(ASSERT_NO_EXCEPTION);
167 range->setEnd(position.containerNode(), position.computeOffsetInContainerNode(), es);
168 if (es.hadException())
170 return TextIterator::rangeLength(range.get());
173 bool TextCheckingParagraph::isEmpty() const
175 // Both predicates should have same result, but we check both just for sure.
176 // We need to investigate to remove this redundancy.
177 return isRangeEmpty() || isTextEmpty();
180 PassRefPtr<Range> TextCheckingParagraph::offsetAsRange() const
182 ASSERT(m_checkingRange);
183 if (!m_offsetAsRange)
184 m_offsetAsRange = Range::create(paragraphRange()->startContainer()->document(), paragraphRange()->startPosition(), checkingRange()->startPosition());
186 return m_offsetAsRange;
189 const String& TextCheckingParagraph::text() const
191 ASSERT(m_checkingRange);
192 if (m_text.isEmpty())
193 m_text = plainText(paragraphRange().get());
197 int TextCheckingParagraph::checkingStart() const
199 ASSERT(m_checkingRange);
200 if (m_checkingStart == -1)
201 m_checkingStart = TextIterator::rangeLength(offsetAsRange().get());
202 return m_checkingStart;
205 int TextCheckingParagraph::checkingEnd() const
207 ASSERT(m_checkingRange);
208 if (m_checkingEnd == -1)
209 m_checkingEnd = checkingStart() + TextIterator::rangeLength(checkingRange().get());
210 return m_checkingEnd;
213 int TextCheckingParagraph::checkingLength() const
215 ASSERT(m_checkingRange);
216 if (-1 == m_checkingLength)
217 m_checkingLength = TextIterator::rangeLength(checkingRange().get());
218 return m_checkingLength;
221 TextCheckingHelper::TextCheckingHelper(EditorClient& client, PassRefPtr<Range> range)
225 ASSERT_ARG(m_range, m_range);
228 TextCheckingHelper::~TextCheckingHelper()
232 String TextCheckingHelper::findFirstMisspelling(int& firstMisspellingOffset, bool markAll, RefPtr<Range>& firstMisspellingRange)
234 WordAwareIterator it(m_range.get());
235 firstMisspellingOffset = 0;
237 String firstMisspelling;
238 int currentChunkOffset = 0;
240 while (!it.atEnd()) {
241 int length = it.length();
243 // Skip some work for one-space-char hunks
244 if (!(length == 1 && it.characterAt(0) == ' ')) {
246 int misspellingLocation = -1;
247 int misspellingLength = 0;
248 m_client->textChecker().checkSpellingOfString(it.substring(0, length), &misspellingLocation, &misspellingLength);
250 // 5490627 shows that there was some code path here where the String constructor below crashes.
251 // We don't know exactly what combination of bad input caused this, so we're making this much
252 // more robust against bad input on release builds.
253 ASSERT(misspellingLength >= 0);
254 ASSERT(misspellingLocation >= -1);
255 ASSERT(!misspellingLength || misspellingLocation >= 0);
256 ASSERT(misspellingLocation < length);
257 ASSERT(misspellingLength <= length);
258 ASSERT(misspellingLocation + misspellingLength <= length);
260 if (misspellingLocation >= 0 && misspellingLength > 0 && misspellingLocation < length && misspellingLength <= length && misspellingLocation + misspellingLength <= length) {
262 // Compute range of misspelled word
263 RefPtr<Range> misspellingRange = TextIterator::subrange(m_range.get(), currentChunkOffset + misspellingLocation, misspellingLength);
265 // Remember first-encountered misspelling and its offset.
266 if (!firstMisspelling) {
267 firstMisspellingOffset = currentChunkOffset + misspellingLocation;
268 firstMisspelling = it.substring(misspellingLocation, misspellingLength);
269 firstMisspellingRange = misspellingRange;
272 // Store marker for misspelled word.
273 misspellingRange->startContainer()->document().markers()->addMarker(misspellingRange.get(), DocumentMarker::Spelling);
275 // Bail out if we're marking only the first misspelling, and not all instances.
281 currentChunkOffset += length;
285 return firstMisspelling;
288 String TextCheckingHelper::findFirstMisspellingOrBadGrammar(bool checkGrammar, bool& outIsSpelling, int& outFirstFoundOffset, GrammarDetail& outGrammarDetail)
290 if (!unifiedTextCheckerEnabled())
293 String firstFoundItem;
294 String misspelledWord;
295 String badGrammarPhrase;
297 // Initialize out parameters; these will be updated if we find something to return.
298 outIsSpelling = true;
299 outFirstFoundOffset = 0;
300 outGrammarDetail.location = -1;
301 outGrammarDetail.length = 0;
302 outGrammarDetail.guesses.clear();
303 outGrammarDetail.userDescription = "";
305 // Expand the search range to encompass entire paragraphs, since text checking needs that much context.
306 // Determine the character offset from the start of the paragraph to the start of the original search range,
307 // since we will want to ignore results in this area.
308 RefPtr<Range> paragraphRange = m_range->cloneRange(IGNORE_EXCEPTION);
309 setStart(paragraphRange.get(), startOfParagraph(m_range->startPosition()));
310 int totalRangeLength = TextIterator::rangeLength(paragraphRange.get());
311 setEnd(paragraphRange.get(), endOfParagraph(m_range->startPosition()));
313 RefPtr<Range> offsetAsRange = Range::create(paragraphRange->startContainer()->document(), paragraphRange->startPosition(), m_range->startPosition());
314 int rangeStartOffset = TextIterator::rangeLength(offsetAsRange.get());
315 int totalLengthProcessed = 0;
317 bool firstIteration = true;
318 bool lastIteration = false;
319 while (totalLengthProcessed < totalRangeLength) {
320 // Iterate through the search range by paragraphs, checking each one for spelling and grammar.
321 int currentLength = TextIterator::rangeLength(paragraphRange.get());
322 int currentStartOffset = firstIteration ? rangeStartOffset : 0;
323 int currentEndOffset = currentLength;
324 if (inSameParagraph(paragraphRange->startPosition(), m_range->endPosition())) {
325 // Determine the character offset from the end of the original search range to the end of the paragraph,
326 // since we will want to ignore results in this area.
327 RefPtr<Range> endOffsetAsRange = Range::create(paragraphRange->startContainer()->document(), paragraphRange->startPosition(), m_range->endPosition());
328 currentEndOffset = TextIterator::rangeLength(endOffsetAsRange.get());
329 lastIteration = true;
331 if (currentStartOffset < currentEndOffset) {
332 String paragraphString = plainText(paragraphRange.get());
333 if (paragraphString.length() > 0) {
334 bool foundGrammar = false;
335 int spellingLocation = 0;
336 int grammarPhraseLocation = 0;
337 int grammarDetailLocation = 0;
338 unsigned grammarDetailIndex = 0;
340 Vector<TextCheckingResult> results;
341 TextCheckingTypeMask checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling;
342 checkTextOfParagraph(m_client->textChecker(), paragraphString, checkingTypes, results);
344 for (unsigned i = 0; i < results.size(); i++) {
345 const TextCheckingResult* result = &results[i];
346 if (result->decoration == TextDecorationTypeSpelling && result->location >= currentStartOffset && result->location + result->length <= currentEndOffset) {
347 ASSERT(result->length > 0 && result->location >= 0);
348 spellingLocation = result->location;
349 misspelledWord = paragraphString.substring(result->location, result->length);
350 ASSERT(misspelledWord.length());
353 if (checkGrammar && result->decoration == TextDecorationTypeGrammar && result->location < currentEndOffset && result->location + result->length > currentStartOffset) {
354 ASSERT(result->length > 0 && result->location >= 0);
355 // We can't stop after the first grammar result, since there might still be a spelling result after
356 // it begins but before the first detail in it, but we can stop if we find a second grammar result.
359 for (unsigned j = 0; j < result->details.size(); j++) {
360 const GrammarDetail* detail = &result->details[j];
361 ASSERT(detail->length > 0 && detail->location >= 0);
362 if (result->location + detail->location >= currentStartOffset && result->location + detail->location + detail->length <= currentEndOffset && (!foundGrammar || result->location + detail->location < grammarDetailLocation)) {
363 grammarDetailIndex = j;
364 grammarDetailLocation = result->location + detail->location;
369 grammarPhraseLocation = result->location;
370 outGrammarDetail = result->details[grammarDetailIndex];
371 badGrammarPhrase = paragraphString.substring(result->location, result->length);
372 ASSERT(badGrammarPhrase.length());
377 if (!misspelledWord.isEmpty() && (!checkGrammar || badGrammarPhrase.isEmpty() || spellingLocation <= grammarDetailLocation)) {
378 int spellingOffset = spellingLocation - currentStartOffset;
379 if (!firstIteration) {
380 RefPtr<Range> paragraphOffsetAsRange = Range::create(paragraphRange->startContainer()->document(), m_range->startPosition(), paragraphRange->startPosition());
381 spellingOffset += TextIterator::rangeLength(paragraphOffsetAsRange.get());
383 outIsSpelling = true;
384 outFirstFoundOffset = spellingOffset;
385 firstFoundItem = misspelledWord;
388 if (checkGrammar && !badGrammarPhrase.isEmpty()) {
389 int grammarPhraseOffset = grammarPhraseLocation - currentStartOffset;
390 if (!firstIteration) {
391 RefPtr<Range> paragraphOffsetAsRange = Range::create(paragraphRange->startContainer()->document(), m_range->startPosition(), paragraphRange->startPosition());
392 grammarPhraseOffset += TextIterator::rangeLength(paragraphOffsetAsRange.get());
394 outIsSpelling = false;
395 outFirstFoundOffset = grammarPhraseOffset;
396 firstFoundItem = badGrammarPhrase;
401 if (lastIteration || totalLengthProcessed + currentLength >= totalRangeLength)
403 VisiblePosition newParagraphStart = startOfNextParagraph(paragraphRange->endPosition());
404 setStart(paragraphRange.get(), newParagraphStart);
405 setEnd(paragraphRange.get(), endOfParagraph(newParagraphStart));
406 firstIteration = false;
407 totalLengthProcessed += currentLength;
409 return firstFoundItem;
412 int TextCheckingHelper::findFirstGrammarDetail(const Vector<GrammarDetail>& grammarDetails, int badGrammarPhraseLocation, int startOffset, int endOffset, bool markAll) const
414 // Found some bad grammar. Find the earliest detail range that starts in our search range (if any).
415 // Optionally add a DocumentMarker for each detail in the range.
416 int earliestDetailLocationSoFar = -1;
417 int earliestDetailIndex = -1;
418 for (unsigned i = 0; i < grammarDetails.size(); i++) {
419 const GrammarDetail* detail = &grammarDetails[i];
420 ASSERT(detail->length > 0 && detail->location >= 0);
422 int detailStartOffsetInParagraph = badGrammarPhraseLocation + detail->location;
424 // Skip this detail if it starts before the original search range
425 if (detailStartOffsetInParagraph < startOffset)
428 // Skip this detail if it starts after the original search range
429 if (detailStartOffsetInParagraph >= endOffset)
433 RefPtr<Range> badGrammarRange = TextIterator::subrange(m_range.get(), badGrammarPhraseLocation - startOffset + detail->location, detail->length);
434 badGrammarRange->startContainer()->document().markers()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription);
437 // Remember this detail only if it's earlier than our current candidate (the details aren't in a guaranteed order)
438 if (earliestDetailIndex < 0 || earliestDetailLocationSoFar > detail->location) {
439 earliestDetailIndex = i;
440 earliestDetailLocationSoFar = detail->location;
444 return earliestDetailIndex;
447 String TextCheckingHelper::findFirstBadGrammar(GrammarDetail& outGrammarDetail, int& outGrammarPhraseOffset, bool markAll)
449 // Initialize out parameters; these will be updated if we find something to return.
450 outGrammarDetail.location = -1;
451 outGrammarDetail.length = 0;
452 outGrammarDetail.guesses.clear();
453 outGrammarDetail.userDescription = "";
454 outGrammarPhraseOffset = 0;
456 String firstBadGrammarPhrase;
458 // Expand the search range to encompass entire paragraphs, since grammar checking needs that much context.
459 // Determine the character offset from the start of the paragraph to the start of the original search range,
460 // since we will want to ignore results in this area.
461 TextCheckingParagraph paragraph(m_range);
463 // Start checking from beginning of paragraph, but skip past results that occur before the start of the original search range.
465 while (startOffset < paragraph.checkingEnd()) {
466 Vector<GrammarDetail> grammarDetails;
467 int badGrammarPhraseLocation = -1;
468 int badGrammarPhraseLength = 0;
469 m_client->textChecker().checkGrammarOfString(paragraph.textSubstring(startOffset), grammarDetails, &badGrammarPhraseLocation, &badGrammarPhraseLength);
471 if (!badGrammarPhraseLength) {
472 ASSERT(badGrammarPhraseLocation == -1);
476 ASSERT(badGrammarPhraseLocation >= 0);
477 badGrammarPhraseLocation += startOffset;
480 // Found some bad grammar. Find the earliest detail range that starts in our search range (if any).
481 int badGrammarIndex = findFirstGrammarDetail(grammarDetails, badGrammarPhraseLocation, paragraph.checkingStart(), paragraph.checkingEnd(), markAll);
482 if (badGrammarIndex >= 0) {
483 ASSERT(static_cast<unsigned>(badGrammarIndex) < grammarDetails.size());
484 outGrammarDetail = grammarDetails[badGrammarIndex];
487 // If we found a detail in range, then we have found the first bad phrase (unless we found one earlier but
488 // kept going so we could mark all instances).
489 if (badGrammarIndex >= 0 && firstBadGrammarPhrase.isEmpty()) {
490 outGrammarPhraseOffset = badGrammarPhraseLocation - paragraph.checkingStart();
491 firstBadGrammarPhrase = paragraph.textSubstring(badGrammarPhraseLocation, badGrammarPhraseLength);
493 // Found one. We're done now, unless we're marking each instance.
498 // These results were all between the start of the paragraph and the start of the search range; look
499 // beyond this phrase.
500 startOffset = badGrammarPhraseLocation + badGrammarPhraseLength;
503 return firstBadGrammarPhrase;
506 void TextCheckingHelper::markAllMisspellings(RefPtr<Range>& firstMisspellingRange)
508 // Use the "markAll" feature of findFirstMisspelling. Ignore the return value and the "out parameter";
509 // all we need to do is mark every instance.
511 findFirstMisspelling(ignoredOffset, true, firstMisspellingRange);
514 void TextCheckingHelper::markAllBadGrammar()
516 // Use the "markAll" feature of ofindFirstBadGrammar. Ignore the return value and "out parameters"; all we need to
517 // do is mark every instance.
518 GrammarDetail ignoredGrammarDetail;
520 findFirstBadGrammar(ignoredGrammarDetail, ignoredOffset, true);
523 bool TextCheckingHelper::unifiedTextCheckerEnabled() const
528 Document& doc = m_range->ownerDocument();
529 return WebCore::unifiedTextCheckerEnabled(doc.frame());
532 void checkTextOfParagraph(TextCheckerClient& client, const String& text, TextCheckingTypeMask checkingTypes, Vector<TextCheckingResult>& results)
534 Vector<UChar> characters;
535 text.appendTo(characters);
536 unsigned length = text.length();
538 Vector<TextCheckingResult> spellingResult;
539 if (checkingTypes & TextCheckingTypeSpelling)
540 findMisspellings(client, characters.data(), 0, length, spellingResult);
542 Vector<TextCheckingResult> grammarResult;
543 if (checkingTypes & TextCheckingTypeGrammar) {
544 // Only checks grammartical error before the first misspellings
545 int grammarCheckLength = length;
546 for (size_t i = 0; i < spellingResult.size(); ++i) {
547 if (spellingResult[i].location < grammarCheckLength)
548 grammarCheckLength = spellingResult[i].location;
551 findBadGrammars(client, characters.data(), 0, grammarCheckLength, grammarResult);
554 if (grammarResult.size())
555 results.swap(grammarResult);
557 if (spellingResult.size()) {
558 if (results.isEmpty())
559 results.swap(spellingResult);
561 results.append(spellingResult);
565 bool unifiedTextCheckerEnabled(const Frame* frame)
570 const Settings* settings = frame->settings();
574 return settings->unifiedTextCheckerEnabled();