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 "TextCheckingHelper.h"
31 #include "TextIterator.h"
32 #include "VisiblePosition.h"
33 #include "visible_units.h"
37 static PassRefPtr<Range> expandToParagraphBoundary(PassRefPtr<Range> range)
40 RefPtr<Range> paragraphRange = range->cloneRange(ec);
41 setStart(paragraphRange.get(), startOfParagraph(range->startPosition()));
42 setEnd(paragraphRange.get(), endOfParagraph(range->endPosition()));
43 return paragraphRange;
46 TextCheckingParagraph::TextCheckingParagraph(PassRefPtr<Range> checkingRange)
47 : m_checkingRange(checkingRange)
50 , m_checkingLength(-1)
54 TextCheckingParagraph::~TextCheckingParagraph()
58 void TextCheckingParagraph::expandRangeToNextEnd()
60 ASSERT(m_checkingRange);
61 setEnd(paragraphRange().get(), endOfParagraph(startOfNextParagraph(paragraphRange()->startPosition())));
62 invalidateParagraphRangeValues();
65 void TextCheckingParagraph::invalidateParagraphRangeValues()
67 m_checkingStart = m_checkingEnd = -1;
72 int TextCheckingParagraph::rangeLength() const
74 ASSERT(m_checkingRange);
75 return TextIterator::rangeLength(paragraphRange().get());
78 PassRefPtr<Range> TextCheckingParagraph::paragraphRange() const
80 ASSERT(m_checkingRange);
81 if (!m_paragraphRange)
82 m_paragraphRange = expandToParagraphBoundary(checkingRange());
83 return m_paragraphRange;
86 PassRefPtr<Range> TextCheckingParagraph::subrange(int characterOffset, int characterCount) const
88 ASSERT(m_checkingRange);
89 return TextIterator::subrange(paragraphRange().get(), characterOffset, characterCount);
92 int TextCheckingParagraph::offsetTo(const Position& position, ExceptionCode& ec) const
94 ASSERT(m_checkingRange);
95 RefPtr<Range> range = offsetAsRange();
96 range->setEnd(position.containerNode(), position.computeOffsetInContainerNode(), ec);
99 return TextIterator::rangeLength(range.get());
102 bool TextCheckingParagraph::isEmpty() const
104 // Both predicates should have same result, but we check both just for sure.
105 // We need to investigate to remove this redundancy.
106 return isRangeEmpty() || isTextEmpty();
109 PassRefPtr<Range> TextCheckingParagraph::offsetAsRange() const
111 ASSERT(m_checkingRange);
112 if (!m_offsetAsRange) {
113 ExceptionCode ec = 0;
114 m_offsetAsRange = Range::create(paragraphRange()->startContainer(ec)->document(), paragraphRange()->startPosition(), checkingRange()->startPosition());
117 return m_offsetAsRange;
120 const String& TextCheckingParagraph::text() const
122 ASSERT(m_checkingRange);
123 if (m_text.isEmpty())
124 m_text = plainText(paragraphRange().get());
128 int TextCheckingParagraph::checkingStart() const
130 ASSERT(m_checkingRange);
131 if (m_checkingStart == -1)
132 m_checkingStart = TextIterator::rangeLength(offsetAsRange().get());
133 return m_checkingStart;
136 int TextCheckingParagraph::checkingEnd() const
138 ASSERT(m_checkingRange);
139 if (m_checkingEnd == -1)
140 m_checkingEnd = checkingStart() + TextIterator::rangeLength(checkingRange().get());
141 return m_checkingEnd;
144 int TextCheckingParagraph::checkingLength() const
146 ASSERT(m_checkingRange);
147 if (-1 == m_checkingLength)
148 m_checkingLength = TextIterator::rangeLength(checkingRange().get());
149 return m_checkingLength;
152 TextCheckingHelper::TextCheckingHelper(EditorClient* client, PassRefPtr<Range> range)
156 ASSERT_ARG(m_client, m_client);
157 ASSERT_ARG(m_range, m_range);
160 TextCheckingHelper::~TextCheckingHelper()
164 String TextCheckingHelper::findFirstMisspelling(int& firstMisspellingOffset, bool markAll, RefPtr<Range>& firstMisspellingRange)
166 WordAwareIterator it(m_range.get());
167 firstMisspellingOffset = 0;
169 String firstMisspelling;
170 int currentChunkOffset = 0;
172 while (!it.atEnd()) {
173 const UChar* chars = it.characters();
174 int len = it.length();
176 // Skip some work for one-space-char hunks
177 if (!(len == 1 && chars[0] == ' ')) {
179 int misspellingLocation = -1;
180 int misspellingLength = 0;
181 m_client->checkSpellingOfString(chars, len, &misspellingLocation, &misspellingLength);
183 // 5490627 shows that there was some code path here where the String constructor below crashes.
184 // We don't know exactly what combination of bad input caused this, so we're making this much
185 // more robust against bad input on release builds.
186 ASSERT(misspellingLength >= 0);
187 ASSERT(misspellingLocation >= -1);
188 ASSERT(!misspellingLength || misspellingLocation >= 0);
189 ASSERT(misspellingLocation < len);
190 ASSERT(misspellingLength <= len);
191 ASSERT(misspellingLocation + misspellingLength <= len);
193 if (misspellingLocation >= 0 && misspellingLength > 0 && misspellingLocation < len && misspellingLength <= len && misspellingLocation + misspellingLength <= len) {
195 // Compute range of misspelled word
196 RefPtr<Range> misspellingRange = TextIterator::subrange(m_range.get(), currentChunkOffset + misspellingLocation, misspellingLength);
198 // Remember first-encountered misspelling and its offset.
199 if (!firstMisspelling) {
200 firstMisspellingOffset = currentChunkOffset + misspellingLocation;
201 firstMisspelling = String(chars + misspellingLocation, misspellingLength);
202 firstMisspellingRange = misspellingRange;
205 // Store marker for misspelled word.
206 ExceptionCode ec = 0;
207 misspellingRange->startContainer(ec)->document()->markers()->addMarker(misspellingRange.get(), DocumentMarker::Spelling);
210 // Bail out if we're marking only the first misspelling, and not all instances.
216 currentChunkOffset += len;
220 return firstMisspelling;
223 String TextCheckingHelper::findFirstMisspellingOrBadGrammar(bool checkGrammar, bool& outIsSpelling, int& outFirstFoundOffset, GrammarDetail& outGrammarDetail)
225 #if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD)
226 String firstFoundItem;
227 String misspelledWord;
228 String badGrammarPhrase;
229 ExceptionCode ec = 0;
231 // Initialize out parameters; these will be updated if we find something to return.
232 outIsSpelling = true;
233 outFirstFoundOffset = 0;
234 outGrammarDetail.location = -1;
235 outGrammarDetail.length = 0;
236 outGrammarDetail.guesses.clear();
237 outGrammarDetail.userDescription = "";
239 // Expand the search range to encompass entire paragraphs, since text checking needs that much context.
240 // Determine the character offset from the start of the paragraph to the start of the original search range,
241 // since we will want to ignore results in this area.
242 RefPtr<Range> paragraphRange = m_range->cloneRange(ec);
243 setStart(paragraphRange.get(), startOfParagraph(m_range->startPosition()));
244 int totalRangeLength = TextIterator::rangeLength(paragraphRange.get());
245 setEnd(paragraphRange.get(), endOfParagraph(m_range->startPosition()));
247 RefPtr<Range> offsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), m_range->startPosition());
248 int rangeStartOffset = TextIterator::rangeLength(offsetAsRange.get());
249 int totalLengthProcessed = 0;
251 bool firstIteration = true;
252 bool lastIteration = false;
253 while (totalLengthProcessed < totalRangeLength) {
254 // Iterate through the search range by paragraphs, checking each one for spelling and grammar.
255 int currentLength = TextIterator::rangeLength(paragraphRange.get());
256 int currentStartOffset = firstIteration ? rangeStartOffset : 0;
257 int currentEndOffset = currentLength;
258 if (inSameParagraph(paragraphRange->startPosition(), m_range->endPosition())) {
259 // Determine the character offset from the end of the original search range to the end of the paragraph,
260 // since we will want to ignore results in this area.
261 RefPtr<Range> endOffsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), paragraphRange->startPosition(), m_range->endPosition());
262 currentEndOffset = TextIterator::rangeLength(endOffsetAsRange.get());
263 lastIteration = true;
265 if (currentStartOffset < currentEndOffset) {
266 String paragraphString = plainText(paragraphRange.get());
267 if (paragraphString.length() > 0) {
268 bool foundGrammar = false;
269 int spellingLocation = 0;
270 int grammarPhraseLocation = 0;
271 int grammarDetailLocation = 0;
272 unsigned grammarDetailIndex = 0;
274 Vector<TextCheckingResult> results;
275 uint64_t checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling;
276 m_client->checkTextOfParagraph(paragraphString.characters(), paragraphString.length(), checkingTypes, results);
278 for (unsigned i = 0; i < results.size(); i++) {
279 const TextCheckingResult* result = &results[i];
280 if (result->type == TextCheckingTypeSpelling && result->location >= currentStartOffset && result->location + result->length <= currentEndOffset) {
281 ASSERT(result->length > 0 && result->location >= 0);
282 spellingLocation = result->location;
283 misspelledWord = paragraphString.substring(result->location, result->length);
284 ASSERT(misspelledWord.length());
287 if (checkGrammar && result->type == TextCheckingTypeGrammar && result->location < currentEndOffset && result->location + result->length > currentStartOffset) {
288 ASSERT(result->length > 0 && result->location >= 0);
289 // We can't stop after the first grammar result, since there might still be a spelling result after
290 // it begins but before the first detail in it, but we can stop if we find a second grammar result.
293 for (unsigned j = 0; j < result->details.size(); j++) {
294 const GrammarDetail* detail = &result->details[j];
295 ASSERT(detail->length > 0 && detail->location >= 0);
296 if (result->location + detail->location >= currentStartOffset && result->location + detail->location + detail->length <= currentEndOffset && (!foundGrammar || result->location + detail->location < grammarDetailLocation)) {
297 grammarDetailIndex = j;
298 grammarDetailLocation = result->location + detail->location;
303 grammarPhraseLocation = result->location;
304 outGrammarDetail = result->details[grammarDetailIndex];
305 badGrammarPhrase = paragraphString.substring(result->location, result->length);
306 ASSERT(badGrammarPhrase.length());
311 if (!misspelledWord.isEmpty() && (!checkGrammar || badGrammarPhrase.isEmpty() || spellingLocation <= grammarDetailLocation)) {
312 int spellingOffset = spellingLocation - currentStartOffset;
313 if (!firstIteration) {
314 RefPtr<Range> paragraphOffsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), m_range->startPosition(), paragraphRange->startPosition());
315 spellingOffset += TextIterator::rangeLength(paragraphOffsetAsRange.get());
317 outIsSpelling = true;
318 outFirstFoundOffset = spellingOffset;
319 firstFoundItem = misspelledWord;
322 if (checkGrammar && !badGrammarPhrase.isEmpty()) {
323 int grammarPhraseOffset = grammarPhraseLocation - currentStartOffset;
324 if (!firstIteration) {
325 RefPtr<Range> paragraphOffsetAsRange = Range::create(paragraphRange->startContainer(ec)->document(), m_range->startPosition(), paragraphRange->startPosition());
326 grammarPhraseOffset += TextIterator::rangeLength(paragraphOffsetAsRange.get());
328 outIsSpelling = false;
329 outFirstFoundOffset = grammarPhraseOffset;
330 firstFoundItem = badGrammarPhrase;
335 if (lastIteration || totalLengthProcessed + currentLength >= totalRangeLength)
337 VisiblePosition newParagraphStart = startOfNextParagraph(paragraphRange->endPosition());
338 setStart(paragraphRange.get(), newParagraphStart);
339 setEnd(paragraphRange.get(), endOfParagraph(newParagraphStart));
340 firstIteration = false;
341 totalLengthProcessed += currentLength;
343 return firstFoundItem;
345 ASSERT_NOT_REACHED();
346 UNUSED_PARAM(checkGrammar);
347 UNUSED_PARAM(outIsSpelling);
348 UNUSED_PARAM(outFirstFoundOffset);
349 UNUSED_PARAM(outGrammarDetail);
354 int TextCheckingHelper::findFirstGrammarDetail(const Vector<GrammarDetail>& grammarDetails, int badGrammarPhraseLocation, int /*badGrammarPhraseLength*/, int startOffset, int endOffset, bool markAll)
356 #ifndef BUILDING_ON_TIGER
357 // Found some bad grammar. Find the earliest detail range that starts in our search range (if any).
358 // Optionally add a DocumentMarker for each detail in the range.
359 int earliestDetailLocationSoFar = -1;
360 int earliestDetailIndex = -1;
361 for (unsigned i = 0; i < grammarDetails.size(); i++) {
362 const GrammarDetail* detail = &grammarDetails[i];
363 ASSERT(detail->length > 0 && detail->location >= 0);
365 int detailStartOffsetInParagraph = badGrammarPhraseLocation + detail->location;
367 // Skip this detail if it starts before the original search range
368 if (detailStartOffsetInParagraph < startOffset)
371 // Skip this detail if it starts after the original search range
372 if (detailStartOffsetInParagraph >= endOffset)
376 RefPtr<Range> badGrammarRange = TextIterator::subrange(m_range.get(), badGrammarPhraseLocation - startOffset + detail->location, detail->length);
377 ExceptionCode ec = 0;
378 badGrammarRange->startContainer(ec)->document()->markers()->addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription);
382 // Remember this detail only if it's earlier than our current candidate (the details aren't in a guaranteed order)
383 if (earliestDetailIndex < 0 || earliestDetailLocationSoFar > detail->location) {
384 earliestDetailIndex = i;
385 earliestDetailLocationSoFar = detail->location;
389 return earliestDetailIndex;
391 ASSERT_NOT_REACHED();
392 UNUSED_PARAM(grammarDetails);
393 UNUSED_PARAM(badGrammarPhraseLocation);
394 UNUSED_PARAM(startOffset);
395 UNUSED_PARAM(endOffset);
396 UNUSED_PARAM(markAll);
401 String TextCheckingHelper::findFirstBadGrammar(GrammarDetail& outGrammarDetail, int& outGrammarPhraseOffset, bool markAll)
403 #ifndef BUILDING_ON_TIGER
404 // Initialize out parameters; these will be updated if we find something to return.
405 outGrammarDetail.location = -1;
406 outGrammarDetail.length = 0;
407 outGrammarDetail.guesses.clear();
408 outGrammarDetail.userDescription = "";
409 outGrammarPhraseOffset = 0;
411 String firstBadGrammarPhrase;
413 // Expand the search range to encompass entire paragraphs, since grammar checking needs that much context.
414 // Determine the character offset from the start of the paragraph to the start of the original search range,
415 // since we will want to ignore results in this area.
416 TextCheckingParagraph paragraph(m_range);
418 // Start checking from beginning of paragraph, but skip past results that occur before the start of the original search range.
420 while (startOffset < paragraph.checkingEnd()) {
421 Vector<GrammarDetail> grammarDetails;
422 int badGrammarPhraseLocation = -1;
423 int badGrammarPhraseLength = 0;
424 m_client->checkGrammarOfString(paragraph.textCharacters() + startOffset, paragraph.textLength() - startOffset, grammarDetails, &badGrammarPhraseLocation, &badGrammarPhraseLength);
426 if (!badGrammarPhraseLength) {
427 ASSERT(badGrammarPhraseLocation == -1);
431 ASSERT(badGrammarPhraseLocation >= 0);
432 badGrammarPhraseLocation += startOffset;
435 // Found some bad grammar. Find the earliest detail range that starts in our search range (if any).
436 int badGrammarIndex = findFirstGrammarDetail(grammarDetails, badGrammarPhraseLocation, badGrammarPhraseLength, paragraph.checkingStart(), paragraph.checkingEnd(), markAll);
437 if (badGrammarIndex >= 0) {
438 ASSERT(static_cast<unsigned>(badGrammarIndex) < grammarDetails.size());
439 outGrammarDetail = grammarDetails[badGrammarIndex];
442 // If we found a detail in range, then we have found the first bad phrase (unless we found one earlier but
443 // kept going so we could mark all instances).
444 if (badGrammarIndex >= 0 && firstBadGrammarPhrase.isEmpty()) {
445 outGrammarPhraseOffset = badGrammarPhraseLocation - paragraph.checkingStart();
446 firstBadGrammarPhrase = paragraph.textSubstring(badGrammarPhraseLocation, badGrammarPhraseLength);
448 // Found one. We're done now, unless we're marking each instance.
453 // These results were all between the start of the paragraph and the start of the search range; look
454 // beyond this phrase.
455 startOffset = badGrammarPhraseLocation + badGrammarPhraseLength;
458 return firstBadGrammarPhrase;
460 ASSERT_NOT_REACHED();
461 UNUSED_PARAM(outGrammarDetail);
462 UNUSED_PARAM(outGrammarPhraseOffset);
463 UNUSED_PARAM(markAll);
468 bool TextCheckingHelper::isUngrammatical(Vector<String>& guessesVector) const
470 #ifndef BUILDING_ON_TIGER
475 if (!m_range || m_range->collapsed(ec))
478 // Returns true only if the passed range exactly corresponds to a bad grammar detail range. This is analogous
479 // to isSelectionMisspelled. It's not good enough for there to be some bad grammar somewhere in the range,
480 // or overlapping the range; the ranges must exactly match.
481 guessesVector.clear();
482 int grammarPhraseOffset;
484 GrammarDetail grammarDetail;
485 String badGrammarPhrase = const_cast<TextCheckingHelper*>(this)->findFirstBadGrammar(grammarDetail, grammarPhraseOffset, false);
487 // No bad grammar in these parts at all.
488 if (badGrammarPhrase.isEmpty())
491 // Bad grammar, but phrase (e.g. sentence) starts beyond start of range.
492 if (grammarPhraseOffset > 0)
495 ASSERT(grammarDetail.location >= 0 && grammarDetail.length > 0);
497 // Bad grammar, but start of detail (e.g. ungrammatical word) doesn't match start of range
498 if (grammarDetail.location + grammarPhraseOffset)
501 // Bad grammar at start of range, but end of bad grammar is before or after end of range
502 if (grammarDetail.length != TextIterator::rangeLength(m_range.get()))
505 // Update the spelling panel to be displaying this error (whether or not the spelling panel is on screen).
506 // This is necessary to make a subsequent call to [NSSpellChecker ignoreWord:inSpellDocumentWithTag:] work
507 // correctly; that call behaves differently based on whether the spelling panel is displaying a misspelling
508 // or a grammar error.
509 m_client->updateSpellingUIWithGrammarString(badGrammarPhrase, grammarDetail);
513 ASSERT_NOT_REACHED();
514 UNUSED_PARAM(guessesVector);
519 Vector<String> TextCheckingHelper::guessesForMisspelledOrUngrammaticalRange(bool checkGrammar, bool& misspelled, bool& ungrammatical) const
521 #if PLATFORM(MAC) && !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD)
522 Vector<String> guesses;
525 ungrammatical = false;
527 if (!m_client || !m_range || m_range->collapsed(ec))
530 // Expand the range to encompass entire paragraphs, since text checking needs that much context.
531 TextCheckingParagraph paragraph(m_range);
532 if (paragraph.isEmpty())
535 Vector<TextCheckingResult> results;
536 uint64_t checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling;
537 m_client->checkTextOfParagraph(paragraph.textCharacters(), paragraph.textLength(), checkingTypes, results);
539 for (unsigned i = 0; i < results.size(); i++) {
540 const TextCheckingResult* result = &results[i];
541 if (result->type == TextCheckingTypeSpelling && paragraph.checkingRangeMatches(result->location, result->length)) {
542 String misspelledWord = paragraph.checkingSubstring();
543 ASSERT(misspelledWord.length());
544 m_client->getGuessesForWord(misspelledWord, String(), guesses);
545 m_client->updateSpellingUIWithMisspelledWord(misspelledWord);
554 for (unsigned i = 0; i < results.size(); i++) {
555 const TextCheckingResult* result = &results[i];
556 if (result->type == TextCheckingTypeGrammar && paragraph.isCheckingRangeCoveredBy(result->location, result->length)) {
557 for (unsigned j = 0; j < result->details.size(); j++) {
558 const GrammarDetail* detail = &result->details[j];
559 ASSERT(detail->length > 0 && detail->location >= 0);
560 if (paragraph.checkingRangeMatches(result->location + detail->location, detail->length)) {
561 String badGrammarPhrase = paragraph.textSubstring(result->location, result->length);
562 ASSERT(badGrammarPhrase.length());
563 for (unsigned k = 0; k < detail->guesses.size(); k++)
564 guesses.append(detail->guesses[k]);
565 m_client->updateSpellingUIWithGrammarString(badGrammarPhrase, *detail);
566 ungrammatical = true;
574 ASSERT_NOT_REACHED();
575 UNUSED_PARAM(checkGrammar);
576 UNUSED_PARAM(misspelled);
577 UNUSED_PARAM(ungrammatical);
578 return Vector<String>();
583 void TextCheckingHelper::markAllMisspellings(RefPtr<Range>& firstMisspellingRange)
585 // Use the "markAll" feature of findFirstMisspelling. Ignore the return value and the "out parameter";
586 // all we need to do is mark every instance.
588 findFirstMisspelling(ignoredOffset, true, firstMisspellingRange);
591 void TextCheckingHelper::markAllBadGrammar()
593 #ifndef BUILDING_ON_TIGER
594 // Use the "markAll" feature of findFirstBadGrammar. Ignore the return value and "out parameters"; all we need to
595 // do is mark every instance.
596 GrammarDetail ignoredGrammarDetail;
598 findFirstBadGrammar(ignoredGrammarDetail, ignoredOffset, true);
600 ASSERT_NOT_REACHED();