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
43namespace WebCore {
44
45#if !USE(UNIFIED_TEXT_CHECKING)
46
47#if USE(GRAMMAR_CHECKING)
48
49static 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
77static 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
112static 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
120TextCheckingParagraph::TextCheckingParagraph(Ref<Range>&& checkingAndAutomaticReplacementRange)
121 : m_checkingRange(checkingAndAutomaticReplacementRange.copyRef())
122 , m_automaticReplacementRange(checkingAndAutomaticReplacementRange.copyRef())
123{
124}
125
126TextCheckingParagraph::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
133void TextCheckingParagraph::expandRangeToNextEnd()
134{
135 setEnd(&paragraphRange(), endOfParagraph(startOfNextParagraph(paragraphRange().startPosition())));
136 invalidateParagraphRangeValues();
137}
138
139void 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
149int TextCheckingParagraph::rangeLength() const
150{
151 return TextIterator::rangeLength(&paragraphRange());
152}
153
154Range& TextCheckingParagraph::paragraphRange() const
155{
156 if (!m_paragraphRange)
157 m_paragraphRange = expandToParagraphBoundary(m_checkingRange);
158 return *m_paragraphRange;
159}
160
161Ref<Range> TextCheckingParagraph::subrange(int characterOffset, int characterCount) const
162{
163 return TextIterator::subrange(paragraphRange(), characterOffset, characterCount);
164}
165
166ExceptionOr<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
178bool 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
185Range& 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
193const String& TextCheckingParagraph::text() const
194{
195 if (m_text.isEmpty())
196 m_text = plainText(&paragraphRange());
197 return m_text;
198}
199
200int TextCheckingParagraph::checkingStart() const
201{
202 if (!m_checkingStart)
203 m_checkingStart = TextIterator::rangeLength(&offsetAsRange());
204 return *m_checkingStart;
205}
206
207int TextCheckingParagraph::checkingEnd() const
208{
209 if (!m_checkingEnd)
210 m_checkingEnd = checkingStart() + TextIterator::rangeLength(m_checkingRange.ptr());
211 return *m_checkingEnd;
212}
213
214int TextCheckingParagraph::checkingLength() const
215{
216 if (!m_checkingLength)
217 m_checkingLength = TextIterator::rangeLength(m_checkingRange.ptr());
218 return *m_checkingLength;
219}
220
221int 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
231int 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
241TextCheckingHelper::TextCheckingHelper(EditorClient& client, Range& range)
242 : m_client(client)
243 , m_range(range)
244{
245}
246
247TextCheckingHelper::~TextCheckingHelper() = default;
248
249String 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 auto 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.ptr();
287 }
288
289 // Store marker for misspelled word.
290 misspellingRange->startContainer().document().markers().addMarker(misspellingRange, 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
301String TextCheckingHelper::findFirstMisspellingOrBadGrammar(bool checkGrammar, bool& outIsSpelling, int& outFirstFoundOffset, GrammarDetail& outGrammarDetail)
302{
303 if (!unifiedTextCheckerEnabled())
304 return emptyString();
305
306 if (platformDrivenTextCheckerEnabled())
307 return emptyString();
308
309 String firstFoundItem;
310 String misspelledWord;
311 String badGrammarPhrase;
312
313 // Initialize out parameters; these will be updated if we find something to return.
314 outIsSpelling = true;
315 outFirstFoundOffset = 0;
316 outGrammarDetail.location = -1;
317 outGrammarDetail.length = 0;
318 outGrammarDetail.guesses.clear();
319 outGrammarDetail.userDescription = emptyString();
320
321 // Expand the search range to encompass entire paragraphs, since text checking needs that much context.
322 // Determine the character offset from the start of the paragraph to the start of the original search range,
323 // since we will want to ignore results in this area.
324 Ref<Range> paragraphRange = m_range->cloneRange();
325 setStart(paragraphRange.ptr(), startOfParagraph(m_range->startPosition()));
326 int totalRangeLength = TextIterator::rangeLength(paragraphRange.ptr());
327 setEnd(paragraphRange.ptr(), endOfParagraph(m_range->startPosition()));
328
329 Ref<Range> offsetAsRange = Range::create(paragraphRange->startContainer().document(), paragraphRange->startPosition(), m_range->startPosition());
330 int rangeStartOffset = TextIterator::rangeLength(offsetAsRange.ptr());
331 int totalLengthProcessed = 0;
332
333 bool firstIteration = true;
334 bool lastIteration = false;
335 while (totalLengthProcessed < totalRangeLength) {
336 // Iterate through the search range by paragraphs, checking each one for spelling and grammar.
337 int currentLength = TextIterator::rangeLength(paragraphRange.ptr());
338 int currentStartOffset = firstIteration ? rangeStartOffset : 0;
339 int currentEndOffset = currentLength;
340 if (inSameParagraph(paragraphRange->startPosition(), m_range->endPosition())) {
341 // Determine the character offset from the end of the original search range to the end of the paragraph,
342 // since we will want to ignore results in this area.
343 auto endOffsetAsRange = Range::create(paragraphRange->startContainer().document(), paragraphRange->startPosition(), m_range->endPosition());
344 currentEndOffset = TextIterator::rangeLength(endOffsetAsRange.ptr());
345 lastIteration = true;
346 }
347 if (currentStartOffset < currentEndOffset) {
348 String paragraphString = plainText(paragraphRange.ptr());
349 if (paragraphString.length() > 0) {
350 bool foundGrammar = false;
351 int spellingLocation = 0;
352 int grammarPhraseLocation = 0;
353 int grammarDetailLocation = 0;
354 unsigned grammarDetailIndex = 0;
355
356 Vector<TextCheckingResult> results;
357 OptionSet<TextCheckingType> checkingTypes { TextCheckingType::Spelling };
358 if (checkGrammar)
359 checkingTypes.add(TextCheckingType::Grammar);
360 VisibleSelection currentSelection;
361 if (Frame* frame = paragraphRange->ownerDocument().frame())
362 currentSelection = frame->selection().selection();
363 checkTextOfParagraph(*m_client.textChecker(), paragraphString, checkingTypes, results, currentSelection);
364
365 for (auto& result : results) {
366 if (result.type == TextCheckingType::Spelling && result.location >= currentStartOffset && result.location + result.length <= currentEndOffset) {
367 ASSERT(result.length > 0);
368 ASSERT(result.location >= 0);
369 spellingLocation = result.location;
370 misspelledWord = paragraphString.substring(result.location, result.length);
371 ASSERT(misspelledWord.length());
372 break;
373 }
374 if (checkGrammar && result.type == TextCheckingType::Grammar && result.location < currentEndOffset && result.location + result.length > currentStartOffset) {
375 ASSERT(result.length > 0);
376 ASSERT(result.location >= 0);
377 // We can't stop after the first grammar result, since there might still be a spelling result after
378 // it begins but before the first detail in it, but we can stop if we find a second grammar result.
379 if (foundGrammar)
380 break;
381 for (unsigned j = 0; j < result.details.size(); j++) {
382 const GrammarDetail* detail = &result.details[j];
383 ASSERT(detail->length > 0);
384 ASSERT(detail->location >= 0);
385 if (result.location + detail->location >= currentStartOffset && result.location + detail->location + detail->length <= currentEndOffset && (!foundGrammar || result.location + detail->location < grammarDetailLocation)) {
386 grammarDetailIndex = j;
387 grammarDetailLocation = result.location + detail->location;
388 foundGrammar = true;
389 }
390 }
391 if (foundGrammar) {
392 grammarPhraseLocation = result.location;
393 outGrammarDetail = result.details[grammarDetailIndex];
394 badGrammarPhrase = paragraphString.substring(result.location, result.length);
395 ASSERT(badGrammarPhrase.length());
396 }
397 }
398 }
399
400 if (!misspelledWord.isEmpty() && (!checkGrammar || badGrammarPhrase.isEmpty() || spellingLocation <= grammarDetailLocation)) {
401 int spellingOffset = spellingLocation - currentStartOffset;
402 if (!firstIteration) {
403 auto paragraphOffsetAsRange = Range::create(paragraphRange->startContainer().document(), m_range->startPosition(), paragraphRange->startPosition());
404 spellingOffset += TextIterator::rangeLength(paragraphOffsetAsRange.ptr());
405 }
406 outIsSpelling = true;
407 outFirstFoundOffset = spellingOffset;
408 firstFoundItem = misspelledWord;
409 break;
410 }
411 if (checkGrammar && !badGrammarPhrase.isEmpty()) {
412 int grammarPhraseOffset = grammarPhraseLocation - currentStartOffset;
413 if (!firstIteration) {
414 auto paragraphOffsetAsRange = Range::create(paragraphRange->startContainer().document(), m_range->startPosition(), paragraphRange->startPosition());
415 grammarPhraseOffset += TextIterator::rangeLength(paragraphOffsetAsRange.ptr());
416 }
417 outIsSpelling = false;
418 outFirstFoundOffset = grammarPhraseOffset;
419 firstFoundItem = badGrammarPhrase;
420 break;
421 }
422 }
423 }
424 if (lastIteration || totalLengthProcessed + currentLength >= totalRangeLength)
425 break;
426 VisiblePosition newParagraphStart = startOfNextParagraph(paragraphRange->endPosition());
427 setStart(paragraphRange.ptr(), newParagraphStart);
428 setEnd(paragraphRange.ptr(), endOfParagraph(newParagraphStart));
429 firstIteration = false;
430 totalLengthProcessed += currentLength;
431 }
432 return firstFoundItem;
433}
434
435#if USE(GRAMMAR_CHECKING)
436
437int TextCheckingHelper::findFirstGrammarDetail(const Vector<GrammarDetail>& grammarDetails, int badGrammarPhraseLocation, int startOffset, int endOffset, bool markAll) const
438{
439 // Found some bad grammar. Find the earliest detail range that starts in our search range (if any).
440 // Optionally add a DocumentMarker for each detail in the range.
441 int earliestDetailLocationSoFar = -1;
442 int earliestDetailIndex = -1;
443 for (unsigned i = 0; i < grammarDetails.size(); i++) {
444 const GrammarDetail* detail = &grammarDetails[i];
445 ASSERT(detail->length > 0);
446 ASSERT(detail->location >= 0);
447
448 int detailStartOffsetInParagraph = badGrammarPhraseLocation + detail->location;
449
450 // Skip this detail if it starts before the original search range
451 if (detailStartOffsetInParagraph < startOffset)
452 continue;
453
454 // Skip this detail if it starts after the original search range
455 if (detailStartOffsetInParagraph >= endOffset)
456 continue;
457
458 if (markAll) {
459 auto badGrammarRange = TextIterator::subrange(m_range, badGrammarPhraseLocation - startOffset + detail->location, detail->length);
460 badGrammarRange->startContainer().document().markers().addMarker(badGrammarRange, DocumentMarker::Grammar, detail->userDescription);
461 }
462
463 // Remember this detail only if it's earlier than our current candidate (the details aren't in a guaranteed order)
464 if (earliestDetailIndex < 0 || earliestDetailLocationSoFar > detail->location) {
465 earliestDetailIndex = i;
466 earliestDetailLocationSoFar = detail->location;
467 }
468 }
469
470 return earliestDetailIndex;
471}
472
473String TextCheckingHelper::findFirstBadGrammar(GrammarDetail& outGrammarDetail, int& outGrammarPhraseOffset, bool markAll) const
474{
475 // Initialize out parameters; these will be updated if we find something to return.
476 outGrammarDetail.location = -1;
477 outGrammarDetail.length = 0;
478 outGrammarDetail.guesses.clear();
479 outGrammarDetail.userDescription = emptyString();
480 outGrammarPhraseOffset = 0;
481
482 String firstBadGrammarPhrase;
483
484 // Expand the search range to encompass entire paragraphs, since grammar checking needs that much context.
485 // Determine the character offset from the start of the paragraph to the start of the original search range,
486 // since we will want to ignore results in this area.
487 TextCheckingParagraph paragraph(m_range.copyRef());
488
489 // Start checking from beginning of paragraph, but skip past results that occur before the start of the original search range.
490 for (int startOffset = 0; startOffset < paragraph.checkingEnd(); ) {
491 Vector<GrammarDetail> grammarDetails;
492 int badGrammarPhraseLocation = -1;
493 int badGrammarPhraseLength = 0;
494 m_client.textChecker()->checkGrammarOfString(StringView(paragraph.text()).substring(startOffset), grammarDetails, &badGrammarPhraseLocation, &badGrammarPhraseLength);
495
496 if (!badGrammarPhraseLength) {
497 ASSERT(badGrammarPhraseLocation == -1);
498 return String();
499 }
500
501 ASSERT(badGrammarPhraseLocation >= 0);
502 badGrammarPhraseLocation += startOffset;
503
504 // Found some bad grammar. Find the earliest detail range that starts in our search range (if any).
505 int badGrammarIndex = findFirstGrammarDetail(grammarDetails, badGrammarPhraseLocation, paragraph.checkingStart(), paragraph.checkingEnd(), markAll);
506 if (badGrammarIndex >= 0) {
507 ASSERT(static_cast<unsigned>(badGrammarIndex) < grammarDetails.size());
508 outGrammarDetail = grammarDetails[badGrammarIndex];
509 }
510
511 // If we found a detail in range, then we have found the first bad phrase (unless we found one earlier but
512 // kept going so we could mark all instances).
513 if (badGrammarIndex >= 0 && firstBadGrammarPhrase.isEmpty()) {
514 outGrammarPhraseOffset = badGrammarPhraseLocation - paragraph.checkingStart();
515 firstBadGrammarPhrase = paragraph.textSubstring(badGrammarPhraseLocation, badGrammarPhraseLength);
516
517 // Found one. We're done now, unless we're marking each instance.
518 if (!markAll)
519 break;
520 }
521
522 // These results were all between the start of the paragraph and the start of the search range; look
523 // beyond this phrase.
524 startOffset = badGrammarPhraseLocation + badGrammarPhraseLength;
525 }
526
527 return firstBadGrammarPhrase;
528}
529
530bool TextCheckingHelper::isUngrammatical() const
531{
532 if (m_range->collapsed())
533 return false;
534
535 // Returns true only if the passed range exactly corresponds to a bad grammar detail range. This is analogous
536 // to isSelectionMisspelled. It's not good enough for there to be some bad grammar somewhere in the range,
537 // or overlapping the range; the ranges must exactly match.
538 int grammarPhraseOffset;
539
540 GrammarDetail grammarDetail;
541 String badGrammarPhrase = findFirstBadGrammar(grammarDetail, grammarPhraseOffset, false);
542
543 // No bad grammar in these parts at all.
544 if (badGrammarPhrase.isEmpty())
545 return false;
546
547 // Bad grammar, but phrase (e.g. sentence) starts beyond start of range.
548 if (grammarPhraseOffset > 0)
549 return false;
550
551 ASSERT(grammarDetail.location >= 0);
552 ASSERT(grammarDetail.length > 0);
553
554 // Bad grammar, but start of detail (e.g. ungrammatical word) doesn't match start of range
555 if (grammarDetail.location + grammarPhraseOffset)
556 return false;
557
558 // Bad grammar at start of range, but end of bad grammar is before or after end of range
559 if (grammarDetail.length != TextIterator::rangeLength(m_range.ptr()))
560 return false;
561
562 // Update the spelling panel to be displaying this error (whether or not the spelling panel is on screen).
563 // This is necessary to make a subsequent call to [NSSpellChecker ignoreWord:inSpellDocumentWithTag:] work
564 // correctly; that call behaves differently based on whether the spelling panel is displaying a misspelling
565 // or a grammar error.
566 m_client.updateSpellingUIWithGrammarString(badGrammarPhrase, grammarDetail);
567
568 return true;
569}
570
571#endif // USE(GRAMMAR_CHECKING)
572
573Vector<String> TextCheckingHelper::guessesForMisspelledOrUngrammaticalRange(bool checkGrammar, bool& misspelled, bool& ungrammatical) const
574{
575 if (!unifiedTextCheckerEnabled())
576 return Vector<String>();
577
578 if (platformDrivenTextCheckerEnabled())
579 return Vector<String>();
580
581 Vector<String> guesses;
582 misspelled = false;
583 ungrammatical = false;
584
585 if (m_range->collapsed())
586 return guesses;
587
588 // Expand the range to encompass entire paragraphs, since text checking needs that much context.
589 TextCheckingParagraph paragraph(m_range.copyRef());
590 if (paragraph.isEmpty())
591 return guesses;
592
593 Vector<TextCheckingResult> results;
594 OptionSet<TextCheckingType> checkingTypes { TextCheckingType::Spelling };
595 if (checkGrammar)
596 checkingTypes.add(TextCheckingType::Grammar);
597 VisibleSelection currentSelection;
598 if (Frame* frame = m_range->ownerDocument().frame())
599 currentSelection = frame->selection().selection();
600 checkTextOfParagraph(*m_client.textChecker(), paragraph.text(), checkingTypes, results, currentSelection);
601
602 for (auto& result : results) {
603 if (result.type == TextCheckingType::Spelling && paragraph.checkingRangeMatches(result.location, result.length)) {
604 String misspelledWord = paragraph.checkingSubstring();
605 ASSERT(misspelledWord.length());
606 m_client.textChecker()->getGuessesForWord(misspelledWord, String(), currentSelection, guesses);
607 m_client.updateSpellingUIWithMisspelledWord(misspelledWord);
608 misspelled = true;
609 return guesses;
610 }
611 }
612
613 if (!checkGrammar)
614 return guesses;
615
616 for (auto& result : results) {
617 if (result.type == TextCheckingType::Grammar && paragraph.isCheckingRangeCoveredBy(result.location, result.length)) {
618 for (auto& detail : result.details) {
619 ASSERT(detail.length > 0);
620 ASSERT(detail.location >= 0);
621 if (paragraph.checkingRangeMatches(result.location + detail.location, detail.length)) {
622 String badGrammarPhrase = paragraph.textSubstring(result.location, result.length);
623 ASSERT(badGrammarPhrase.length());
624 for (auto& guess : detail.guesses)
625 guesses.append(guess);
626 m_client.updateSpellingUIWithGrammarString(badGrammarPhrase, detail);
627 ungrammatical = true;
628 return guesses;
629 }
630 }
631 }
632 }
633 return guesses;
634}
635
636void TextCheckingHelper::markAllMisspellings(RefPtr<Range>& firstMisspellingRange)
637{
638 // Use the "markAll" feature of findFirstMisspelling. Ignore the return value and the "out parameter";
639 // all we need to do is mark every instance.
640 int ignoredOffset;
641 findFirstMisspelling(ignoredOffset, true, firstMisspellingRange);
642}
643
644#if USE(GRAMMAR_CHECKING)
645void TextCheckingHelper::markAllBadGrammar()
646{
647 // Use the "markAll" feature of ofindFirstBadGrammar. Ignore the return value and "out parameters"; all we need to
648 // do is mark every instance.
649 GrammarDetail ignoredGrammarDetail;
650 int ignoredOffset;
651 findFirstBadGrammar(ignoredGrammarDetail, ignoredOffset, true);
652}
653#endif
654
655bool TextCheckingHelper::unifiedTextCheckerEnabled() const
656{
657 return WebCore::unifiedTextCheckerEnabled(m_range->ownerDocument().frame());
658}
659
660void checkTextOfParagraph(TextCheckerClient& client, StringView text, OptionSet<TextCheckingType> checkingTypes, Vector<TextCheckingResult>& results, const VisibleSelection& currentSelection)
661{
662#if USE(UNIFIED_TEXT_CHECKING)
663 results = client.checkTextOfParagraph(text, checkingTypes, currentSelection);
664#else
665 UNUSED_PARAM(currentSelection);
666
667 Vector<TextCheckingResult> mispellings;
668 if (checkingTypes.contains(TextCheckingType::Spelling))
669 findMisspellings(client, text, mispellings);
670
671#if USE(GRAMMAR_CHECKING)
672 // Look for grammatical errors that occur before the first misspelling.
673 Vector<TextCheckingResult> grammaticalErrors;
674 if (checkingTypes.contains(TextCheckingType::Grammar)) {
675 unsigned grammarCheckLength = text.length();
676 for (auto& mispelling : mispellings)
677 grammarCheckLength = std::min<unsigned>(grammarCheckLength, mispelling.location);
678 findGrammaticalErrors(client, text.substring(0, grammarCheckLength), grammaticalErrors);
679 }
680
681 results = WTFMove(grammaticalErrors);
682#endif
683
684 if (results.isEmpty())
685 results = WTFMove(mispellings);
686 else
687 results.appendVector(mispellings);
688#endif // USE(UNIFIED_TEXT_CHECKING)
689}
690
691bool unifiedTextCheckerEnabled(const Frame* frame)
692{
693 if (!frame)
694 return false;
695 return frame->settings().unifiedTextCheckerEnabled();
696}
697
698bool platformDrivenTextCheckerEnabled()
699{
700#if ENABLE(PLATFORM_DRIVEN_TEXT_CHECKING)
701 return true;
702#else
703 return false;
704#endif
705}
706
707}
708