05a6cba790df27f33e18110a9da7d50f4b148478
[WebKit-https.git] / Source / WebCore / editing / TextCheckingHelper.cpp
1 /*
2  * Copyright (C) 2006, 2007, 2014 Apple Inc. All rights reserved.
3  * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
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.
13  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. 
25  */
26
27 #include "config.h"
28 #include "TextCheckingHelper.h"
29
30 #include "Document.h"
31 #include "DocumentMarkerController.h"
32 #include "Frame.h"
33 #include "FrameSelection.h"
34 #include "Settings.h"
35 #include "TextCheckerClient.h"
36 #include "TextIterator.h"
37 #include "VisiblePosition.h"
38 #include "VisibleUnits.h"
39 #include <unicode/ubrk.h>
40 #include <wtf/text/StringView.h>
41 #include <wtf/text/TextBreakIterator.h>
42
43 namespace WebCore {
44
45 #if !USE(UNIFIED_TEXT_CHECKING)
46
47 #if USE(GRAMMAR_CHECKING)
48
49 static void findGrammaticalErrors(TextCheckerClient& client, StringView text, Vector<TextCheckingResult>& results)
50 {
51     for (unsigned checkLocation = 0; checkLocation < text.length(); ) {
52         int badGrammarLocation = -1;
53         int badGrammarLength = 0;
54         Vector<GrammarDetail> badGrammarDetails;
55         client.checkGrammarOfString(text.substring(checkLocation), badGrammarDetails, &badGrammarLocation, &badGrammarLength);
56         if (!badGrammarLength)
57             break;
58
59         ASSERT(badGrammarLocation >= 0);
60         ASSERT(static_cast<unsigned>(badGrammarLocation) <= text.length() - checkLocation);
61         ASSERT(badGrammarLength > 0);
62         ASSERT(static_cast<unsigned>(badGrammarLength) <= text.length() - checkLocation - badGrammarLocation);
63
64         TextCheckingResult badGrammar;
65         badGrammar.type = TextCheckingType::Grammar;
66         badGrammar.location = checkLocation + badGrammarLocation;
67         badGrammar.length = badGrammarLength;
68         badGrammar.details = WTFMove(badGrammarDetails);
69         results.append(badGrammar);
70
71         checkLocation += badGrammarLocation + badGrammarLength;
72     }
73 }
74
75 #endif
76
77 static void findMisspellings(TextCheckerClient& client, StringView text, Vector<TextCheckingResult>& results)
78 {
79     UBreakIterator* iterator = wordBreakIterator(text);
80     if (!iterator)
81         return;
82     for (int wordStart = ubrk_current(iterator); wordStart >= 0; ) {
83         int wordEnd = ubrk_next(iterator);
84         if (wordEnd < 0)
85             break;
86
87         int wordLength = wordEnd - wordStart;
88         int misspellingLocation = -1;
89         int misspellingLength = 0;
90         client.checkSpellingOfString(text.substring(wordStart, wordLength), &misspellingLocation, &misspellingLength);
91
92         if (misspellingLength > 0) {
93             ASSERT(misspellingLocation >= 0);
94             ASSERT(misspellingLocation <= wordLength);
95             ASSERT(misspellingLength > 0);
96             ASSERT(misspellingLocation + misspellingLength <= wordLength);
97
98             TextCheckingResult misspelling;
99             misspelling.type = TextCheckingType::Spelling;
100             misspelling.location = wordStart + misspellingLocation;
101             misspelling.length = misspellingLength;
102             misspelling.replacement = client.getAutoCorrectSuggestionForMisspelledWord(text.substring(misspelling.location, misspelling.length).toStringWithoutCopying());
103             results.append(misspelling);
104         }
105
106         wordStart = wordEnd;
107     }
108 }
109
110 #endif
111
112 static Ref<Range> expandToParagraphBoundary(Range& range)
113 {
114     Ref<Range> paragraphRange = range.cloneRange();
115     setStart(paragraphRange.ptr(), startOfParagraph(range.startPosition()));
116     setEnd(paragraphRange.ptr(), endOfParagraph(range.endPosition()));
117     return paragraphRange;
118 }
119
120 TextCheckingParagraph::TextCheckingParagraph(Ref<Range>&& checkingAndAutomaticReplacementRange)
121     : m_checkingRange(checkingAndAutomaticReplacementRange.copyRef())
122     , m_automaticReplacementRange(checkingAndAutomaticReplacementRange.copyRef())
123 {
124 }
125
126 TextCheckingParagraph::TextCheckingParagraph(Ref<Range>&& checkingRange, Ref<Range>&& automaticReplacementRange, RefPtr<Range>&& paragraphRange)
127     : m_checkingRange(WTFMove(checkingRange))
128     , m_automaticReplacementRange(WTFMove(automaticReplacementRange))
129     , m_paragraphRange(WTFMove(paragraphRange))
130 {
131 }
132
133 void TextCheckingParagraph::expandRangeToNextEnd()
134 {
135     setEnd(&paragraphRange(), endOfParagraph(startOfNextParagraph(paragraphRange().startPosition())));
136     invalidateParagraphRangeValues();
137 }
138
139 void TextCheckingParagraph::invalidateParagraphRangeValues()
140 {
141     m_checkingStart.reset();
142     m_checkingEnd.reset();
143     m_automaticReplacementStart.reset();
144     m_automaticReplacementLength.reset();
145     m_offsetAsRange = nullptr;
146     m_text = String();
147 }
148
149 int TextCheckingParagraph::rangeLength() const
150 {
151     return TextIterator::rangeLength(&paragraphRange());
152 }
153
154 Range& TextCheckingParagraph::paragraphRange() const
155 {
156     if (!m_paragraphRange)
157         m_paragraphRange = expandToParagraphBoundary(m_checkingRange);
158     return *m_paragraphRange;
159 }
160
161 Ref<Range> TextCheckingParagraph::subrange(int characterOffset, int characterCount) const
162 {
163     return TextIterator::subrange(paragraphRange(), characterOffset, characterCount);
164 }
165
166 ExceptionOr<int> TextCheckingParagraph::offsetTo(const Position& position) const
167 {
168     if (!position.containerNode())
169         return Exception { TypeError };
170
171     auto range = offsetAsRange().cloneRange();
172     auto result = range->setEnd(*position.containerNode(), position.computeOffsetInContainerNode());
173     if (result.hasException())
174         return result.releaseException();
175     return TextIterator::rangeLength(range.ptr());
176 }
177
178 bool TextCheckingParagraph::isEmpty() const
179 {
180     // Both predicates should have same result, but we check both just for sure.
181     // We need to investigate to remove this redundancy.
182     return checkingStart() >= checkingEnd() || text().isEmpty();
183 }
184
185 Range& TextCheckingParagraph::offsetAsRange() const
186 {
187     if (!m_offsetAsRange)
188         m_offsetAsRange = Range::create(paragraphRange().startContainer().document(), paragraphRange().startPosition(), m_checkingRange->startPosition());
189
190     return *m_offsetAsRange;
191 }
192
193 const String& TextCheckingParagraph::text() const
194 {
195     if (m_text.isEmpty())
196         m_text = plainText(&paragraphRange());
197     return m_text; 
198 }
199
200 int TextCheckingParagraph::checkingStart() const
201 {
202     if (!m_checkingStart)
203         m_checkingStart = TextIterator::rangeLength(&offsetAsRange());
204     return *m_checkingStart;
205 }
206
207 int TextCheckingParagraph::checkingEnd() const
208 {
209     if (!m_checkingEnd)
210         m_checkingEnd = checkingStart() + TextIterator::rangeLength(m_checkingRange.ptr());
211     return *m_checkingEnd;
212 }
213
214 int TextCheckingParagraph::checkingLength() const
215 {
216     if (!m_checkingLength)
217         m_checkingLength = TextIterator::rangeLength(m_checkingRange.ptr());
218     return *m_checkingLength;
219 }
220
221 int TextCheckingParagraph::automaticReplacementStart() const
222 {
223     if (m_automaticReplacementStart)
224         return *m_automaticReplacementStart;
225
226     auto startOffsetRange = Range::create(paragraphRange().startContainer().document(), paragraphRange().startPosition(), m_automaticReplacementRange->startPosition());
227     m_automaticReplacementStart = TextIterator::rangeLength(startOffsetRange.ptr());
228     return *m_automaticReplacementStart;
229 }
230
231 int TextCheckingParagraph::automaticReplacementLength() const
232 {
233     if (m_automaticReplacementLength)
234         return *m_automaticReplacementLength;
235
236     auto endOffsetRange = Range::create(paragraphRange().startContainer().document(), paragraphRange().startPosition(), m_automaticReplacementRange->endPosition());
237     m_automaticReplacementLength = TextIterator::rangeLength(endOffsetRange.ptr()) - automaticReplacementStart();
238     return *m_automaticReplacementLength;
239 }
240
241 TextCheckingHelper::TextCheckingHelper(EditorClient& client, Range& range)
242     : m_client(client)
243     , m_range(range)
244 {
245 }
246
247 TextCheckingHelper::~TextCheckingHelper() = default;
248
249 String TextCheckingHelper::findFirstMisspelling(int& firstMisspellingOffset, bool markAll, RefPtr<Range>& firstMisspellingRange)
250 {
251     firstMisspellingOffset = 0;
252
253     String firstMisspelling;
254     int currentChunkOffset = 0;
255
256     for (WordAwareIterator it(m_range); !it.atEnd(); currentChunkOffset += it.text().length(), it.advance()) {
257         StringView text = it.text();
258         int textLength = text.length();
259
260         // Skip some work for one-space-char hunks.
261         if (textLength == 1 && text[0] == ' ')
262             continue;
263
264         int misspellingLocation = -1;
265         int misspellingLength = 0;
266         m_client.textChecker()->checkSpellingOfString(text, &misspellingLocation, &misspellingLength);
267
268         // 5490627 shows that there was some code path here where the String constructor below crashes.
269         // We don't know exactly what combination of bad input caused this, so we're making this much
270         // more robust against bad input on release builds.
271         ASSERT(misspellingLength >= 0);
272         ASSERT(misspellingLocation >= -1);
273         ASSERT(!misspellingLength || misspellingLocation >= 0);
274         ASSERT(misspellingLocation < textLength);
275         ASSERT(misspellingLength <= textLength);
276         ASSERT(misspellingLocation + misspellingLength <= textLength);
277
278         if (misspellingLocation >= 0 && misspellingLength > 0 && misspellingLocation < textLength && misspellingLength <= textLength && misspellingLocation + misspellingLength <= textLength) {
279             // Compute range of misspelled word
280             RefPtr<Range> misspellingRange = TextIterator::subrange(m_range, currentChunkOffset + misspellingLocation, misspellingLength);
281
282             // Remember first-encountered misspelling and its offset.
283             if (!firstMisspelling) {
284                 firstMisspellingOffset = currentChunkOffset + misspellingLocation;
285                 firstMisspelling = text.substring(misspellingLocation, misspellingLength).toString();
286                 firstMisspellingRange = misspellingRange;
287             }
288
289             // Store marker for misspelled word.
290             misspellingRange->startContainer().document().markers().addMarker(misspellingRange.get(), DocumentMarker::Spelling);
291
292             // Bail out if we're marking only the first misspelling, and not all instances.
293             if (!markAll)
294                 break;
295         }
296     }
297
298     return firstMisspelling;
299 }
300
301 String TextCheckingHelper::findFirstMisspellingOrBadGrammar(bool checkGrammar, bool& outIsSpelling, int& outFirstFoundOffset, GrammarDetail& outGrammarDetail)
302 {
303     if (!unifiedTextCheckerEnabled())
304         return emptyString();
305
306     String firstFoundItem;
307     String misspelledWord;
308     String badGrammarPhrase;
309     
310     // Initialize out parameters; these will be updated if we find something to return.
311     outIsSpelling = true;
312     outFirstFoundOffset = 0;
313     outGrammarDetail.location = -1;
314     outGrammarDetail.length = 0;
315     outGrammarDetail.guesses.clear();
316     outGrammarDetail.userDescription = emptyString();
317     
318     // Expand the search range to encompass entire paragraphs, since text checking needs that much context.
319     // Determine the character offset from the start of the paragraph to the start of the original search range,
320     // since we will want to ignore results in this area.
321     Ref<Range> paragraphRange = m_range->cloneRange();
322     setStart(paragraphRange.ptr(), startOfParagraph(m_range->startPosition()));
323     int totalRangeLength = TextIterator::rangeLength(paragraphRange.ptr());
324     setEnd(paragraphRange.ptr(), endOfParagraph(m_range->startPosition()));
325     
326     Ref<Range> offsetAsRange = Range::create(paragraphRange->startContainer().document(), paragraphRange->startPosition(), m_range->startPosition());
327     int rangeStartOffset = TextIterator::rangeLength(offsetAsRange.ptr());
328     int totalLengthProcessed = 0;
329     
330     bool firstIteration = true;
331     bool lastIteration = false;
332     while (totalLengthProcessed < totalRangeLength) {
333         // Iterate through the search range by paragraphs, checking each one for spelling and grammar.
334         int currentLength = TextIterator::rangeLength(paragraphRange.ptr());
335         int currentStartOffset = firstIteration ? rangeStartOffset : 0;
336         int currentEndOffset = currentLength;
337         if (inSameParagraph(paragraphRange->startPosition(), m_range->endPosition())) {
338             // Determine the character offset from the end of the original search range to the end of the paragraph,
339             // since we will want to ignore results in this area.
340             RefPtr<Range> endOffsetAsRange = Range::create(paragraphRange->startContainer().document(), paragraphRange->startPosition(), m_range->endPosition());
341             currentEndOffset = TextIterator::rangeLength(endOffsetAsRange.get());
342             lastIteration = true;
343         }
344         if (currentStartOffset < currentEndOffset) {
345             String paragraphString = plainText(paragraphRange.ptr());
346             if (paragraphString.length() > 0) {
347                 bool foundGrammar = false;
348                 int spellingLocation = 0;
349                 int grammarPhraseLocation = 0;
350                 int grammarDetailLocation = 0;
351                 unsigned grammarDetailIndex = 0;
352                 
353                 Vector<TextCheckingResult> results;
354                 OptionSet<TextCheckingType> checkingTypes { TextCheckingType::Spelling };
355                 if (checkGrammar)
356                     checkingTypes |= TextCheckingType::Grammar;
357                 VisibleSelection currentSelection;
358                 if (Frame* frame = paragraphRange->ownerDocument().frame())
359                     currentSelection = frame->selection().selection();
360                 checkTextOfParagraph(*m_client.textChecker(), paragraphString, checkingTypes, results, currentSelection);
361
362                 for (auto& result : results) {
363                     if (result.type == TextCheckingType::Spelling && result.location >= currentStartOffset && result.location + result.length <= currentEndOffset) {
364                         ASSERT(result.length > 0);
365                         ASSERT(result.location >= 0);
366                         spellingLocation = result.location;
367                         misspelledWord = paragraphString.substring(result.location, result.length);
368                         ASSERT(misspelledWord.length());
369                         break;
370                     }
371                     if (checkGrammar && result.type == TextCheckingType::Grammar && result.location < currentEndOffset && result.location + result.length > currentStartOffset) {
372                         ASSERT(result.length > 0);
373                         ASSERT(result.location >= 0);
374                         // We can't stop after the first grammar result, since there might still be a spelling result after
375                         // it begins but before the first detail in it, but we can stop if we find a second grammar result.
376                         if (foundGrammar)
377                             break;
378                         for (unsigned j = 0; j < result.details.size(); j++) {
379                             const GrammarDetail* detail = &result.details[j];
380                             ASSERT(detail->length > 0);
381                             ASSERT(detail->location >= 0);
382                             if (result.location + detail->location >= currentStartOffset && result.location + detail->location + detail->length <= currentEndOffset && (!foundGrammar || result.location + detail->location < grammarDetailLocation)) {
383                                 grammarDetailIndex = j;
384                                 grammarDetailLocation = result.location + detail->location;
385                                 foundGrammar = true;
386                             }
387                         }
388                         if (foundGrammar) {
389                             grammarPhraseLocation = result.location;
390                             outGrammarDetail = result.details[grammarDetailIndex];
391                             badGrammarPhrase = paragraphString.substring(result.location, result.length);
392                             ASSERT(badGrammarPhrase.length());
393                         }
394                     }
395                 }
396
397                 if (!misspelledWord.isEmpty() && (!checkGrammar || badGrammarPhrase.isEmpty() || spellingLocation <= grammarDetailLocation)) {
398                     int spellingOffset = spellingLocation - currentStartOffset;
399                     if (!firstIteration) {
400                         RefPtr<Range> paragraphOffsetAsRange = Range::create(paragraphRange->startContainer().document(), m_range->startPosition(), paragraphRange->startPosition());
401                         spellingOffset += TextIterator::rangeLength(paragraphOffsetAsRange.get());
402                     }
403                     outIsSpelling = true;
404                     outFirstFoundOffset = spellingOffset;
405                     firstFoundItem = misspelledWord;
406                     break;
407                 }
408                 if (checkGrammar && !badGrammarPhrase.isEmpty()) {
409                     int grammarPhraseOffset = grammarPhraseLocation - currentStartOffset;
410                     if (!firstIteration) {
411                         RefPtr<Range> paragraphOffsetAsRange = Range::create(paragraphRange->startContainer().document(), m_range->startPosition(), paragraphRange->startPosition());
412                         grammarPhraseOffset += TextIterator::rangeLength(paragraphOffsetAsRange.get());
413                     }
414                     outIsSpelling = false;
415                     outFirstFoundOffset = grammarPhraseOffset;
416                     firstFoundItem = badGrammarPhrase;
417                     break;
418                 }
419             }
420         }
421         if (lastIteration || totalLengthProcessed + currentLength >= totalRangeLength)
422             break;
423         VisiblePosition newParagraphStart = startOfNextParagraph(paragraphRange->endPosition());
424         setStart(paragraphRange.ptr(), newParagraphStart);
425         setEnd(paragraphRange.ptr(), endOfParagraph(newParagraphStart));
426         firstIteration = false;
427         totalLengthProcessed += currentLength;
428     }
429     return firstFoundItem;
430 }
431
432 #if USE(GRAMMAR_CHECKING)
433
434 int TextCheckingHelper::findFirstGrammarDetail(const Vector<GrammarDetail>& grammarDetails, int badGrammarPhraseLocation, int startOffset, int endOffset, bool markAll) const
435 {
436     // Found some bad grammar. Find the earliest detail range that starts in our search range (if any).
437     // Optionally add a DocumentMarker for each detail in the range.
438     int earliestDetailLocationSoFar = -1;
439     int earliestDetailIndex = -1;
440     for (unsigned i = 0; i < grammarDetails.size(); i++) {
441         const GrammarDetail* detail = &grammarDetails[i];
442         ASSERT(detail->length > 0);
443         ASSERT(detail->location >= 0);
444         
445         int detailStartOffsetInParagraph = badGrammarPhraseLocation + detail->location;
446         
447         // Skip this detail if it starts before the original search range
448         if (detailStartOffsetInParagraph < startOffset)
449             continue;
450         
451         // Skip this detail if it starts after the original search range
452         if (detailStartOffsetInParagraph >= endOffset)
453             continue;
454         
455         if (markAll) {
456             RefPtr<Range> badGrammarRange = TextIterator::subrange(m_range, badGrammarPhraseLocation - startOffset + detail->location, detail->length);
457             badGrammarRange->startContainer().document().markers().addMarker(badGrammarRange.get(), DocumentMarker::Grammar, detail->userDescription);
458         }
459         
460         // Remember this detail only if it's earlier than our current candidate (the details aren't in a guaranteed order)
461         if (earliestDetailIndex < 0 || earliestDetailLocationSoFar > detail->location) {
462             earliestDetailIndex = i;
463             earliestDetailLocationSoFar = detail->location;
464         }
465     }
466     
467     return earliestDetailIndex;
468 }
469
470 String TextCheckingHelper::findFirstBadGrammar(GrammarDetail& outGrammarDetail, int& outGrammarPhraseOffset, bool markAll) const
471 {
472     // Initialize out parameters; these will be updated if we find something to return.
473     outGrammarDetail.location = -1;
474     outGrammarDetail.length = 0;
475     outGrammarDetail.guesses.clear();
476     outGrammarDetail.userDescription = emptyString();
477     outGrammarPhraseOffset = 0;
478     
479     String firstBadGrammarPhrase;
480
481     // Expand the search range to encompass entire paragraphs, since grammar checking needs that much context.
482     // Determine the character offset from the start of the paragraph to the start of the original search range,
483     // since we will want to ignore results in this area.
484     TextCheckingParagraph paragraph(m_range.copyRef());
485     
486     // Start checking from beginning of paragraph, but skip past results that occur before the start of the original search range.
487     for (int startOffset = 0; startOffset < paragraph.checkingEnd(); ) {
488         Vector<GrammarDetail> grammarDetails;
489         int badGrammarPhraseLocation = -1;
490         int badGrammarPhraseLength = 0;
491         m_client.textChecker()->checkGrammarOfString(StringView(paragraph.text()).substring(startOffset), grammarDetails, &badGrammarPhraseLocation, &badGrammarPhraseLength);
492         
493         if (!badGrammarPhraseLength) {
494             ASSERT(badGrammarPhraseLocation == -1);
495             return String();
496         }
497
498         ASSERT(badGrammarPhraseLocation >= 0);
499         badGrammarPhraseLocation += startOffset;
500
501         // Found some bad grammar. Find the earliest detail range that starts in our search range (if any).
502         int badGrammarIndex = findFirstGrammarDetail(grammarDetails, badGrammarPhraseLocation, paragraph.checkingStart(), paragraph.checkingEnd(), markAll);
503         if (badGrammarIndex >= 0) {
504             ASSERT(static_cast<unsigned>(badGrammarIndex) < grammarDetails.size());
505             outGrammarDetail = grammarDetails[badGrammarIndex];
506         }
507
508         // If we found a detail in range, then we have found the first bad phrase (unless we found one earlier but
509         // kept going so we could mark all instances).
510         if (badGrammarIndex >= 0 && firstBadGrammarPhrase.isEmpty()) {
511             outGrammarPhraseOffset = badGrammarPhraseLocation - paragraph.checkingStart();
512             firstBadGrammarPhrase = paragraph.textSubstring(badGrammarPhraseLocation, badGrammarPhraseLength);
513             
514             // Found one. We're done now, unless we're marking each instance.
515             if (!markAll)
516                 break;
517         }
518
519         // These results were all between the start of the paragraph and the start of the search range; look
520         // beyond this phrase.
521         startOffset = badGrammarPhraseLocation + badGrammarPhraseLength;
522     }
523     
524     return firstBadGrammarPhrase;
525 }
526
527 bool TextCheckingHelper::isUngrammatical() const
528 {
529     if (m_range->collapsed())
530         return false;
531     
532     // Returns true only if the passed range exactly corresponds to a bad grammar detail range. This is analogous
533     // to isSelectionMisspelled. It's not good enough for there to be some bad grammar somewhere in the range,
534     // or overlapping the range; the ranges must exactly match.
535     int grammarPhraseOffset;
536     
537     GrammarDetail grammarDetail;
538     String badGrammarPhrase = findFirstBadGrammar(grammarDetail, grammarPhraseOffset, false);
539     
540     // No bad grammar in these parts at all.
541     if (badGrammarPhrase.isEmpty())
542         return false;
543     
544     // Bad grammar, but phrase (e.g. sentence) starts beyond start of range.
545     if (grammarPhraseOffset > 0)
546         return false;
547
548     ASSERT(grammarDetail.location >= 0);
549     ASSERT(grammarDetail.length > 0);
550
551     // Bad grammar, but start of detail (e.g. ungrammatical word) doesn't match start of range
552     if (grammarDetail.location + grammarPhraseOffset)
553         return false;
554     
555     // Bad grammar at start of range, but end of bad grammar is before or after end of range
556     if (grammarDetail.length != TextIterator::rangeLength(m_range.ptr()))
557         return false;
558     
559     // Update the spelling panel to be displaying this error (whether or not the spelling panel is on screen).
560     // This is necessary to make a subsequent call to [NSSpellChecker ignoreWord:inSpellDocumentWithTag:] work
561     // correctly; that call behaves differently based on whether the spelling panel is displaying a misspelling
562     // or a grammar error.
563     m_client.updateSpellingUIWithGrammarString(badGrammarPhrase, grammarDetail);
564     
565     return true;
566 }
567
568 #endif // USE(GRAMMAR_CHECKING)
569
570 Vector<String> TextCheckingHelper::guessesForMisspelledOrUngrammaticalRange(bool checkGrammar, bool& misspelled, bool& ungrammatical) const
571 {
572     if (!unifiedTextCheckerEnabled())
573         return Vector<String>();
574
575     Vector<String> guesses;
576     misspelled = false;
577     ungrammatical = false;
578     
579     if (m_range->collapsed())
580         return guesses;
581
582     // Expand the range to encompass entire paragraphs, since text checking needs that much context.
583     TextCheckingParagraph paragraph(m_range.copyRef());
584     if (paragraph.isEmpty())
585         return guesses;
586
587     Vector<TextCheckingResult> results;
588     OptionSet<TextCheckingType> checkingTypes { TextCheckingType::Spelling };
589     if (checkGrammar)
590         checkingTypes |= TextCheckingType::Grammar;
591     VisibleSelection currentSelection;
592     if (Frame* frame = m_range->ownerDocument().frame())
593         currentSelection = frame->selection().selection();
594     checkTextOfParagraph(*m_client.textChecker(), paragraph.text(), checkingTypes, results, currentSelection);
595
596     for (auto& result : results) {
597         if (result.type == TextCheckingType::Spelling && paragraph.checkingRangeMatches(result.location, result.length)) {
598             String misspelledWord = paragraph.checkingSubstring();
599             ASSERT(misspelledWord.length());
600             m_client.textChecker()->getGuessesForWord(misspelledWord, String(), currentSelection, guesses);
601             m_client.updateSpellingUIWithMisspelledWord(misspelledWord);
602             misspelled = true;
603             return guesses;
604         }
605     }
606     
607     if (!checkGrammar)
608         return guesses;
609
610     for (auto& result : results) {
611         if (result.type == TextCheckingType::Grammar && paragraph.isCheckingRangeCoveredBy(result.location, result.length)) {
612             for (auto& detail : result.details) {
613                 ASSERT(detail.length > 0);
614                 ASSERT(detail.location >= 0);
615                 if (paragraph.checkingRangeMatches(result.location + detail.location, detail.length)) {
616                     String badGrammarPhrase = paragraph.textSubstring(result.location, result.length);
617                     ASSERT(badGrammarPhrase.length());
618                     for (auto& guess : detail.guesses)
619                         guesses.append(guess);
620                     m_client.updateSpellingUIWithGrammarString(badGrammarPhrase, detail);
621                     ungrammatical = true;
622                     return guesses;
623                 }
624             }
625         }
626     }
627     return guesses;
628 }
629
630 void TextCheckingHelper::markAllMisspellings(RefPtr<Range>& firstMisspellingRange)
631 {
632     // Use the "markAll" feature of findFirstMisspelling. Ignore the return value and the "out parameter";
633     // all we need to do is mark every instance.
634     int ignoredOffset;
635     findFirstMisspelling(ignoredOffset, true, firstMisspellingRange);
636 }
637
638 #if USE(GRAMMAR_CHECKING)
639 void TextCheckingHelper::markAllBadGrammar()
640 {
641     // Use the "markAll" feature of ofindFirstBadGrammar. Ignore the return value and "out parameters"; all we need to
642     // do is mark every instance.
643     GrammarDetail ignoredGrammarDetail;
644     int ignoredOffset;
645     findFirstBadGrammar(ignoredGrammarDetail, ignoredOffset, true);
646 }
647 #endif
648
649 bool TextCheckingHelper::unifiedTextCheckerEnabled() const
650 {
651     return WebCore::unifiedTextCheckerEnabled(m_range->ownerDocument().frame());
652 }
653
654 void checkTextOfParagraph(TextCheckerClient& client, StringView text, OptionSet<TextCheckingType> checkingTypes, Vector<TextCheckingResult>& results, const VisibleSelection& currentSelection)
655 {
656 #if USE(UNIFIED_TEXT_CHECKING)
657     results = client.checkTextOfParagraph(text, checkingTypes, currentSelection);
658 #else
659     UNUSED_PARAM(currentSelection);
660
661     Vector<TextCheckingResult> mispellings;
662     if (checkingTypes.contains(TextCheckingType::Spelling))
663         findMisspellings(client, text, mispellings);
664
665 #if USE(GRAMMAR_CHECKING)
666     // Look for grammatical errors that occur before the first misspelling.
667     Vector<TextCheckingResult> grammaticalErrors;
668     if (checkingTypes.contains(TextCheckingType::Grammar)) {
669         unsigned grammarCheckLength = text.length();
670         for (auto& mispelling : mispellings)
671             grammarCheckLength = std::min<unsigned>(grammarCheckLength, mispelling.location);
672         findGrammaticalErrors(client, text.substring(0, grammarCheckLength), grammaticalErrors);
673     }
674
675     results = WTFMove(grammaticalErrors);
676 #endif
677
678     if (results.isEmpty())
679         results = WTFMove(mispellings);
680     else
681         results.appendVector(mispellings);
682 #endif // USE(UNIFIED_TEXT_CHECKING)
683 }
684
685 bool unifiedTextCheckerEnabled(const Frame* frame)
686 {
687     if (!frame)
688         return false;
689     return frame->settings().unifiedTextCheckerEnabled();
690 }
691
692 }