1/*
2 * Copyright (C) 2010, 2011 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#import "config.h"
27#import "TextChecker.h"
28
29#if PLATFORM(MAC)
30
31#import "TextCheckerState.h"
32#import <WebCore/NotImplemented.h>
33#import <wtf/RetainPtr.h>
34#import <wtf/text/StringView.h>
35
36@interface NSSpellChecker (WebNSSpellCheckerDetails)
37- (NSString *)languageForWordRange:(NSRange)range inString:(NSString *)string orthography:(NSOrthography *)orthography;
38@end
39
40static NSString* const WebAutomaticSpellingCorrectionEnabled = @"WebAutomaticSpellingCorrectionEnabled";
41static NSString* const WebContinuousSpellCheckingEnabled = @"WebContinuousSpellCheckingEnabled";
42static NSString* const WebGrammarCheckingEnabled = @"WebGrammarCheckingEnabled";
43static NSString* const WebSmartInsertDeleteEnabled = @"WebSmartInsertDeleteEnabled";
44static NSString* const WebAutomaticQuoteSubstitutionEnabled = @"WebAutomaticQuoteSubstitutionEnabled";
45static NSString* const WebAutomaticDashSubstitutionEnabled = @"WebAutomaticDashSubstitutionEnabled";
46static NSString* const WebAutomaticLinkDetectionEnabled = @"WebAutomaticLinkDetectionEnabled";
47static NSString* const WebAutomaticTextReplacementEnabled = @"WebAutomaticTextReplacementEnabled";
48
49using namespace WebCore;
50
51namespace WebKit {
52
53TextCheckerState textCheckerState;
54
55static bool shouldAutomaticTextReplacementBeEnabled()
56{
57    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
58    if (![defaults objectForKey:WebAutomaticTextReplacementEnabled])
59        return [NSSpellChecker isAutomaticTextReplacementEnabled];
60    return [defaults boolForKey:WebAutomaticTextReplacementEnabled];
61}
62
63static bool shouldAutomaticSpellingCorrectionBeEnabled()
64{
65    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
66    if (![defaults objectForKey:WebAutomaticSpellingCorrectionEnabled])
67        return [NSSpellChecker isAutomaticTextReplacementEnabled];
68    return [defaults boolForKey:WebAutomaticSpellingCorrectionEnabled];
69}
70
71static bool shouldAutomaticQuoteSubstitutionBeEnabled()
72{
73    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
74#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 1090
75    if (![defaults objectForKey:WebAutomaticQuoteSubstitutionEnabled])
76        return [NSSpellChecker isAutomaticQuoteSubstitutionEnabled];
77#endif
78    return [defaults boolForKey:WebAutomaticQuoteSubstitutionEnabled];
79}
80
81static bool shouldAutomaticDashSubstitutionBeEnabled()
82{
83    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
84#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 1090
85    if (![defaults objectForKey:WebAutomaticDashSubstitutionEnabled])
86        return [NSSpellChecker isAutomaticDashSubstitutionEnabled];
87#endif
88    return [defaults boolForKey:WebAutomaticDashSubstitutionEnabled];
89}
90
91static void initializeState()
92{
93    static bool didInitializeState = false;
94
95    if (didInitializeState)
96        return;
97
98    textCheckerState.isContinuousSpellCheckingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:WebContinuousSpellCheckingEnabled] && TextChecker::isContinuousSpellCheckingAllowed();
99    textCheckerState.isGrammarCheckingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:WebGrammarCheckingEnabled];
100    textCheckerState.isAutomaticTextReplacementEnabled = shouldAutomaticTextReplacementBeEnabled();
101    textCheckerState.isAutomaticSpellingCorrectionEnabled = shouldAutomaticSpellingCorrectionBeEnabled();
102    textCheckerState.isAutomaticQuoteSubstitutionEnabled = shouldAutomaticQuoteSubstitutionBeEnabled();
103    textCheckerState.isAutomaticDashSubstitutionEnabled = shouldAutomaticDashSubstitutionBeEnabled();
104    textCheckerState.isAutomaticLinkDetectionEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:WebAutomaticLinkDetectionEnabled];
105
106    didInitializeState = true;
107}
108
109const TextCheckerState& TextChecker::state()
110{
111    initializeState();
112    return textCheckerState;
113}
114
115bool TextChecker::isContinuousSpellCheckingAllowed()
116{
117    static bool allowContinuousSpellChecking = true;
118    static bool readAllowContinuousSpellCheckingDefault = false;
119
120    if (!readAllowContinuousSpellCheckingDefault) {
121        if ([[NSUserDefaults standardUserDefaults] objectForKey:@"NSAllowContinuousSpellChecking"])
122            allowContinuousSpellChecking = [[NSUserDefaults standardUserDefaults] boolForKey:@"NSAllowContinuousSpellChecking"];
123
124        readAllowContinuousSpellCheckingDefault = true;
125    }
126
127    return allowContinuousSpellChecking;
128}
129
130void TextChecker::setContinuousSpellCheckingEnabled(bool isContinuousSpellCheckingEnabled)
131{
132    if (state().isContinuousSpellCheckingEnabled == isContinuousSpellCheckingEnabled)
133        return;
134
135    textCheckerState.isContinuousSpellCheckingEnabled = isContinuousSpellCheckingEnabled;
136    [[NSUserDefaults standardUserDefaults] setBool:isContinuousSpellCheckingEnabled forKey:WebContinuousSpellCheckingEnabled];
137
138    // FIXME: preflight the spell checker.
139}
140
141void TextChecker::setGrammarCheckingEnabled(bool isGrammarCheckingEnabled)
142{
143    if (state().isGrammarCheckingEnabled == isGrammarCheckingEnabled)
144        return;
145
146    textCheckerState.isGrammarCheckingEnabled = isGrammarCheckingEnabled;
147    [[NSUserDefaults standardUserDefaults] setBool:isGrammarCheckingEnabled forKey:WebGrammarCheckingEnabled];
148    [[NSSpellChecker sharedSpellChecker] updatePanels];
149
150    // We call preflightSpellChecker() when turning continuous spell checking on, but we don't need to do that here
151    // because grammar checking only occurs on code paths that already preflight spell checking appropriately.
152}
153
154void TextChecker::setAutomaticSpellingCorrectionEnabled(bool isAutomaticSpellingCorrectionEnabled)
155{
156    if (state().isAutomaticSpellingCorrectionEnabled == isAutomaticSpellingCorrectionEnabled)
157        return;
158
159    textCheckerState.isAutomaticSpellingCorrectionEnabled = isAutomaticSpellingCorrectionEnabled;
160    [[NSUserDefaults standardUserDefaults] setBool:isAutomaticSpellingCorrectionEnabled forKey:WebAutomaticSpellingCorrectionEnabled];
161
162    [[NSSpellChecker sharedSpellChecker] updatePanels];
163}
164
165void TextChecker::setAutomaticQuoteSubstitutionEnabled(bool isAutomaticQuoteSubstitutionEnabled)
166{
167    if (state().isAutomaticQuoteSubstitutionEnabled == isAutomaticQuoteSubstitutionEnabled)
168        return;
169
170    textCheckerState.isAutomaticQuoteSubstitutionEnabled = isAutomaticQuoteSubstitutionEnabled;
171    [[NSUserDefaults standardUserDefaults] setBool:isAutomaticQuoteSubstitutionEnabled forKey:WebAutomaticQuoteSubstitutionEnabled];
172
173    [[NSSpellChecker sharedSpellChecker] updatePanels];
174}
175
176void TextChecker::setAutomaticDashSubstitutionEnabled(bool isAutomaticDashSubstitutionEnabled)
177{
178    if (state().isAutomaticDashSubstitutionEnabled == isAutomaticDashSubstitutionEnabled)
179        return;
180
181    textCheckerState.isAutomaticDashSubstitutionEnabled = isAutomaticDashSubstitutionEnabled;
182    [[NSUserDefaults standardUserDefaults] setBool:isAutomaticDashSubstitutionEnabled forKey:WebAutomaticDashSubstitutionEnabled];
183
184    [[NSSpellChecker sharedSpellChecker] updatePanels];
185}
186
187void TextChecker::setAutomaticLinkDetectionEnabled(bool isAutomaticLinkDetectionEnabled)
188{
189    if (state().isAutomaticLinkDetectionEnabled == isAutomaticLinkDetectionEnabled)
190        return;
191
192    textCheckerState.isAutomaticLinkDetectionEnabled = isAutomaticLinkDetectionEnabled;
193    [[NSUserDefaults standardUserDefaults] setBool:isAutomaticLinkDetectionEnabled forKey:WebAutomaticLinkDetectionEnabled];
194
195    [[NSSpellChecker sharedSpellChecker] updatePanels];
196}
197
198void TextChecker::setAutomaticTextReplacementEnabled(bool isAutomaticTextReplacementEnabled)
199{
200    if (state().isAutomaticTextReplacementEnabled == isAutomaticTextReplacementEnabled)
201        return;
202
203    textCheckerState.isAutomaticTextReplacementEnabled = isAutomaticTextReplacementEnabled;
204    [[NSUserDefaults standardUserDefaults] setBool:isAutomaticTextReplacementEnabled forKey:WebAutomaticTextReplacementEnabled];
205
206    [[NSSpellChecker sharedSpellChecker] updatePanels];
207}
208
209static bool smartInsertDeleteEnabled;
210
211bool TextChecker::isSmartInsertDeleteEnabled()
212{
213    static bool readSmartInsertDeleteEnabledDefault;
214
215    if (!readSmartInsertDeleteEnabledDefault) {
216        smartInsertDeleteEnabled = ![[NSUserDefaults standardUserDefaults] objectForKey:WebSmartInsertDeleteEnabled] || [[NSUserDefaults standardUserDefaults] boolForKey:WebSmartInsertDeleteEnabled];
217
218        readSmartInsertDeleteEnabledDefault = true;
219    }
220
221    return smartInsertDeleteEnabled;
222}
223
224void TextChecker::setSmartInsertDeleteEnabled(bool flag)
225{
226    if (flag == isSmartInsertDeleteEnabled())
227        return;
228
229    smartInsertDeleteEnabled = flag;
230
231    [[NSUserDefaults standardUserDefaults] setBool:flag forKey:WebSmartInsertDeleteEnabled];
232}
233
234void TextChecker::didChangeAutomaticTextReplacementEnabled()
235{
236    textCheckerState.isAutomaticTextReplacementEnabled = shouldAutomaticTextReplacementBeEnabled();
237    [[NSSpellChecker sharedSpellChecker] updatePanels];
238}
239
240void TextChecker::didChangeAutomaticSpellingCorrectionEnabled()
241{
242    textCheckerState.isAutomaticSpellingCorrectionEnabled = shouldAutomaticSpellingCorrectionBeEnabled();
243    [[NSSpellChecker sharedSpellChecker] updatePanels];
244}
245
246void TextChecker::didChangeAutomaticQuoteSubstitutionEnabled()
247{
248#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 1090
249    textCheckerState.isAutomaticQuoteSubstitutionEnabled = shouldAutomaticQuoteSubstitutionBeEnabled();
250    [[NSSpellChecker sharedSpellChecker] updatePanels];
251#endif
252}
253
254void TextChecker::didChangeAutomaticDashSubstitutionEnabled()
255{
256#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 1090
257    textCheckerState.isAutomaticDashSubstitutionEnabled = shouldAutomaticDashSubstitutionBeEnabled();
258    [[NSSpellChecker sharedSpellChecker] updatePanels];
259#endif
260}
261
262bool TextChecker::substitutionsPanelIsShowing()
263{
264    return [[[NSSpellChecker sharedSpellChecker] substitutionsPanel] isVisible];
265}
266
267void TextChecker::toggleSubstitutionsPanelIsShowing()
268{
269    NSPanel *substitutionsPanel = [[NSSpellChecker sharedSpellChecker] substitutionsPanel];
270    if ([substitutionsPanel isVisible]) {
271        [substitutionsPanel orderOut:nil];
272        return;
273    }
274    [substitutionsPanel orderFront:nil];
275}
276
277void TextChecker::continuousSpellCheckingEnabledStateChanged(bool enabled)
278{
279    textCheckerState.isContinuousSpellCheckingEnabled = enabled;
280}
281
282void TextChecker::grammarCheckingEnabledStateChanged(bool enabled)
283{
284    textCheckerState.isGrammarCheckingEnabled = enabled;
285}
286
287int64_t TextChecker::uniqueSpellDocumentTag(WebPageProxy*)
288{
289    return [NSSpellChecker uniqueSpellDocumentTag];
290}
291
292void TextChecker::closeSpellDocumentWithTag(int64_t tag)
293{
294    [[NSSpellChecker sharedSpellChecker] closeSpellDocumentWithTag:tag];
295}
296
297#if USE(UNIFIED_TEXT_CHECKING)
298
299Vector<TextCheckingResult> TextChecker::checkTextOfParagraph(int64_t spellDocumentTag, StringView text, uint64_t checkingTypes)
300{
301    Vector<TextCheckingResult> results;
302
303    RetainPtr<NSString> textString = text.createNSStringWithoutCopying();
304    NSArray *incomingResults = [[NSSpellChecker sharedSpellChecker] checkString:textString.get()
305                                                                          range:NSMakeRange(0, text.length())
306                                                                          types:checkingTypes | NSTextCheckingTypeOrthography
307                                                                        options:nil
308                                                         inSpellDocumentWithTag:spellDocumentTag
309                                                                    orthography:NULL
310                                                                      wordCount:NULL];
311    for (NSTextCheckingResult *incomingResult in incomingResults) {
312        NSRange resultRange = [incomingResult range];
313        NSTextCheckingType resultType = [incomingResult resultType];
314        ASSERT(resultRange.location != NSNotFound);
315        ASSERT(resultRange.length > 0);
316        if (resultType == NSTextCheckingTypeSpelling && (checkingTypes & NSTextCheckingTypeSpelling)) {
317            TextCheckingResult result;
318            result.type = TextCheckingTypeSpelling;
319            result.location = resultRange.location;
320            result.length = resultRange.length;
321            results.append(result);
322        } else if (resultType == NSTextCheckingTypeGrammar && (checkingTypes & NSTextCheckingTypeGrammar)) {
323            TextCheckingResult result;
324            NSArray *details = [incomingResult grammarDetails];
325            result.type = TextCheckingTypeGrammar;
326            result.location = resultRange.location;
327            result.length = resultRange.length;
328            for (NSDictionary *incomingDetail in details) {
329                ASSERT(incomingDetail);
330                GrammarDetail detail;
331                NSValue *detailRangeAsNSValue = [incomingDetail objectForKey:NSGrammarRange];
332                ASSERT(detailRangeAsNSValue);
333                NSRange detailNSRange = [detailRangeAsNSValue rangeValue];
334                ASSERT(detailNSRange.location != NSNotFound);
335                ASSERT(detailNSRange.length > 0);
336                detail.location = detailNSRange.location;
337                detail.length = detailNSRange.length;
338                detail.userDescription = [incomingDetail objectForKey:NSGrammarUserDescription];
339                NSArray *guesses = [incomingDetail objectForKey:NSGrammarCorrections];
340                for (NSString *guess in guesses)
341                    detail.guesses.append(String(guess));
342                result.details.append(detail);
343            }
344            results.append(result);
345        } else if (resultType == NSTextCheckingTypeLink && (checkingTypes & NSTextCheckingTypeLink)) {
346            TextCheckingResult result;
347            result.type = TextCheckingTypeLink;
348            result.location = resultRange.location;
349            result.length = resultRange.length;
350            result.replacement = [[incomingResult URL] absoluteString];
351            results.append(result);
352        } else if (resultType == NSTextCheckingTypeQuote && (checkingTypes & NSTextCheckingTypeQuote)) {
353            TextCheckingResult result;
354            result.type = TextCheckingTypeQuote;
355            result.location = resultRange.location;
356            result.length = resultRange.length;
357            result.replacement = [incomingResult replacementString];
358            results.append(result);
359        } else if (resultType == NSTextCheckingTypeDash && (checkingTypes & NSTextCheckingTypeDash)) {
360            TextCheckingResult result;
361            result.type = TextCheckingTypeDash;
362            result.location = resultRange.location;
363            result.length = resultRange.length;
364            result.replacement = [incomingResult replacementString];
365            results.append(result);
366        } else if (resultType == NSTextCheckingTypeReplacement && (checkingTypes & NSTextCheckingTypeReplacement)) {
367            TextCheckingResult result;
368            result.type = TextCheckingTypeReplacement;
369            result.location = resultRange.location;
370            result.length = resultRange.length;
371            result.replacement = [incomingResult replacementString];
372            results.append(result);
373        } else if (resultType == NSTextCheckingTypeCorrection && (checkingTypes & NSTextCheckingTypeCorrection)) {
374            TextCheckingResult result;
375            result.type = TextCheckingTypeCorrection;
376            result.location = resultRange.location;
377            result.length = resultRange.length;
378            result.replacement = [incomingResult replacementString];
379            results.append(result);
380        }
381    }
382
383    return results;
384}
385
386#endif
387
388void TextChecker::checkSpellingOfString(int64_t, StringView, int32_t&, int32_t&)
389{
390    // Mac uses checkTextOfParagraph instead.
391    notImplemented();
392}
393
394void TextChecker::checkGrammarOfString(int64_t, StringView, Vector<WebCore::GrammarDetail>&, int32_t&, int32_t&)
395{
396    // Mac uses checkTextOfParagraph instead.
397    notImplemented();
398}
399
400bool TextChecker::spellingUIIsShowing()
401{
402    return [[[NSSpellChecker sharedSpellChecker] spellingPanel] isVisible];
403}
404
405void TextChecker::toggleSpellingUIIsShowing()
406{
407    NSPanel *spellingPanel = [[NSSpellChecker sharedSpellChecker] spellingPanel];
408    if ([spellingPanel isVisible])
409        [spellingPanel orderOut:nil];
410    else
411        [spellingPanel orderFront:nil];
412}
413
414void TextChecker::updateSpellingUIWithMisspelledWord(int64_t, const String& misspelledWord)
415{
416    [[NSSpellChecker sharedSpellChecker] updateSpellingPanelWithMisspelledWord:misspelledWord];
417}
418
419void TextChecker::updateSpellingUIWithGrammarString(int64_t, const String& badGrammarPhrase, const GrammarDetail& grammarDetail)
420{
421    RetainPtr<NSMutableArray> corrections = adoptNS([[NSMutableArray alloc] init]);
422    for (size_t i = 0; i < grammarDetail.guesses.size(); ++i) {
423        NSString *guess = grammarDetail.guesses[i];
424        [corrections addObject:guess];
425    }
426
427    NSRange grammarRange = NSMakeRange(grammarDetail.location, grammarDetail.length);
428    NSString *grammarUserDescription = grammarDetail.userDescription;
429    RetainPtr<NSDictionary> grammarDetailDict = adoptNS([[NSDictionary alloc] initWithObjectsAndKeys:[NSValue valueWithRange:grammarRange], NSGrammarRange, grammarUserDescription, NSGrammarUserDescription, corrections.get(), NSGrammarCorrections, nil]);
430
431    [[NSSpellChecker sharedSpellChecker] updateSpellingPanelWithGrammarString:badGrammarPhrase detail:grammarDetailDict.get()];
432}
433
434void TextChecker::getGuessesForWord(int64_t spellDocumentTag, const String& word, const String& context, Vector<String>& guesses)
435{
436    NSString* language = nil;
437    NSOrthography* orthography = nil;
438    NSSpellChecker *checker = [NSSpellChecker sharedSpellChecker];
439    if (context.length()) {
440        [checker checkString:context range:NSMakeRange(0, context.length()) types:NSTextCheckingTypeOrthography options:0 inSpellDocumentWithTag:spellDocumentTag orthography:&orthography wordCount:0];
441        language = [checker languageForWordRange:NSMakeRange(0, context.length()) inString:context orthography:orthography];
442    }
443    NSArray* stringsArray = [checker guessesForWordRange:NSMakeRange(0, word.length()) inString:word language:language inSpellDocumentWithTag:spellDocumentTag];
444
445    for (NSString *guess in stringsArray)
446        guesses.append(guess);
447}
448
449void TextChecker::learnWord(int64_t, const String& word)
450{
451    [[NSSpellChecker sharedSpellChecker] learnWord:word];
452}
453
454void TextChecker::ignoreWord(int64_t spellDocumentTag, const String& word)
455{
456    [[NSSpellChecker sharedSpellChecker] ignoreWord:word inSpellDocumentWithTag:spellDocumentTag];
457}
458
459void TextChecker::requestCheckingOfString(PassRefPtr<TextCheckerCompletion>)
460{
461    notImplemented();
462}
463
464} // namespace WebKit
465
466#endif // PLATFORM(MAC)
467