blob: ec66d0aadcc2676cdc345a9eddfcd7729261b839 [file] [log] [blame] [edit]
/*
* Copyright (C) 2018 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "LayoutTestSpellChecker.h"
#import "InstanceMethodSwizzler.h"
#import "JSBasics.h"
#import <objc/runtime.h>
#import <pal/spi/mac/NSSpellCheckerSPI.h>
#import <wtf/Assertions.h>
#import <wtf/BlockPtr.h>
#if PLATFORM(IOS_FAMILY)
#import "UIKitSPIForTesting.h"
#endif
#if PLATFORM(MAC)
using TextCheckingCompletionHandler = void(^)(NSInteger, NSArray<NSTextCheckingResult *> *, NSOrthography *, NSInteger);
#else
#define NSGrammarRange @"NSGrammarRange"
#define NSGrammarCorrections @"NSGrammarCorrections"
#endif
static LayoutTestSpellChecker *globalSpellChecker = nil;
static BOOL hasSwizzledLayoutTestSpellChecker = NO;
#if PLATFORM(MAC)
static IMP globallySwizzledSharedSpellCheckerImplementation;
static Method originalSharedSpellCheckerMethod;
#else
static IMP globallySwizzledInitializeTextCheckerImplementation;
static Method originalInitializeTextCheckerMethod;
@protocol TextComposerPostEditorProtocol <NSObject>
- (NSDictionary<NSString *, id> *)grammarDetailForString:(NSString *)stringToCheck range:(NSRange)range language:(NSString *)language;
@end
@interface UITextChecker (TestingSupport)
@property (readonly, nonatomic) id<TextComposerPostEditorProtocol> postEditor;
@end
#endif
static LayoutTestSpellChecker *ensureGlobalLayoutTestSpellChecker()
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
globalSpellChecker = [[LayoutTestSpellChecker alloc] init];
});
return globalSpellChecker;
}
static NSTextCheckingType nsTextCheckingType(NSString *typeString)
{
if ([typeString isEqualToString:@"orthography"])
return NSTextCheckingTypeOrthography;
if ([typeString isEqualToString:@"spelling"])
return NSTextCheckingTypeSpelling;
if ([typeString isEqualToString:@"grammar"])
return NSTextCheckingTypeGrammar;
if ([typeString isEqualToString:@"date"])
return NSTextCheckingTypeDate;
if ([typeString isEqualToString:@"address"])
return NSTextCheckingTypeAddress;
if ([typeString isEqualToString:@"link"])
return NSTextCheckingTypeLink;
if ([typeString isEqualToString:@"quote"])
return NSTextCheckingTypeQuote;
if ([typeString isEqualToString:@"dash"])
return NSTextCheckingTypeDash;
if ([typeString isEqualToString:@"replacement"])
return NSTextCheckingTypeReplacement;
if ([typeString isEqualToString:@"correction"])
return NSTextCheckingTypeCorrection;
if ([typeString isEqualToString:@"regular-expression"])
return NSTextCheckingTypeRegularExpression;
if ([typeString isEqualToString:@"phone-number"])
return NSTextCheckingTypePhoneNumber;
if ([typeString isEqualToString:@"transit-information"])
return NSTextCheckingTypeTransitInformation;
ASSERT_NOT_REACHED();
return NSTextCheckingTypeSpelling;
}
@interface LayoutTestTextCheckingResult : NSTextCheckingResult
- (instancetype)initWithType:(NSTextCheckingType)type range:(NSRange)range replacement:(NSString *)replacement details:(NSArray<NSDictionary<NSString *, id> *> *)details;
@end
@implementation LayoutTestTextCheckingResult {
RetainPtr<NSString> _replacement;
NSTextCheckingType _type;
NSRange _range;
RetainPtr<NSArray<NSDictionary *>> _details;
}
- (instancetype)initWithType:(NSTextCheckingType)type range:(NSRange)range replacement:(NSString *)replacement details:(NSArray<NSDictionary<NSString *, id> *> *)details
{
if (!(self = [super init]))
return nil;
_type = type;
_range = range;
_replacement = adoptNS(replacement.copy);
_details = adoptNS(details.copy);
return self;
}
- (NSArray<NSDictionary<NSString *, id> *> *)grammarDetails
{
return _details.get();
}
- (NSRange)range
{
return _range;
}
- (NSTextCheckingType)resultType
{
return _type;
}
- (NSString *)replacementString
{
return _replacement.get();
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@ %p type=%llu range=[%u, %u] replacement='%@'>", self.class, self, _type, static_cast<unsigned>(_range.location), static_cast<unsigned>(_range.location + _range.length), _replacement.get()];
}
@end
@implementation LayoutTestSpellChecker
@synthesize spellCheckerLoggingEnabled = _spellCheckerLoggingEnabled;
#if PLATFORM(IOS_FAMILY)
static LayoutTestSpellChecker *swizzledInitializeTextChecker()
{
return [ensureGlobalLayoutTestSpellChecker() retain];
}
#endif
+ (instancetype)checker
{
auto *spellChecker = ensureGlobalLayoutTestSpellChecker();
if (hasSwizzledLayoutTestSpellChecker)
return spellChecker;
#if PLATFORM(MAC)
originalSharedSpellCheckerMethod = class_getClassMethod(objc_getMetaClass("NSSpellChecker"), @selector(sharedSpellChecker));
globallySwizzledSharedSpellCheckerImplementation = method_setImplementation(originalSharedSpellCheckerMethod, reinterpret_cast<IMP>(ensureGlobalLayoutTestSpellChecker));
#else
originalInitializeTextCheckerMethod = class_getInstanceMethod(UITextChecker.class, @selector(_initWithAsynchronousLoading:));
globallySwizzledInitializeTextCheckerImplementation = method_setImplementation(originalInitializeTextCheckerMethod, reinterpret_cast<IMP>(swizzledInitializeTextChecker));
#endif
hasSwizzledLayoutTestSpellChecker = YES;
return spellChecker;
}
+ (void)uninstallAndReset
{
[globalSpellChecker reset];
if (!hasSwizzledLayoutTestSpellChecker)
return;
#if PLATFORM(MAC)
method_setImplementation(originalSharedSpellCheckerMethod, globallySwizzledSharedSpellCheckerImplementation);
#else
method_setImplementation(originalInitializeTextCheckerMethod, globallySwizzledInitializeTextCheckerImplementation);
#endif
hasSwizzledLayoutTestSpellChecker = NO;
}
- (void)reset
{
self.results = nil;
self.spellCheckerLoggingEnabled = NO;
}
- (TextCheckingResultsDictionary *)results
{
return _results.get();
}
- (void)setResults:(TextCheckingResultsDictionary *)results
{
_results = adoptNS(results.copy);
}
- (void)setResultsFromJSValue:(JSValueRef)resultsValue inContext:(JSContextRef)jsContext
{
auto context = [JSContext contextWithJSGlobalContextRef:JSContextGetGlobalContext(jsContext)];
auto resultsDictionary = [JSValue valueWithJSValueRef:resultsValue inContext:context].toDictionary;
auto finalResults = adoptNS([NSMutableDictionary new]);
for (NSString *stringToCheck in resultsDictionary) {
auto resultsForWord = adoptNS([NSMutableArray new]);
for (NSDictionary *result in resultsDictionary[stringToCheck]) {
auto from = [result[@"from"] intValue];
auto to = [result[@"to"] intValue];
NSString *type = result[@"type"];
NSString *replacement = result[@"replacement"];
auto details = adoptNS([NSMutableArray<NSDictionary *> new]);
for (NSDictionary *detail in result[@"details"]) {
NSNumber *detailFrom = detail[@"from"];
NSNumber *detailTo = detail[@"to"];
auto detailRange = NSMakeRange(detailFrom.intValue, detailTo.intValue - detailFrom.intValue);
[details addObject:@{ NSGrammarRange: [NSValue valueWithRange:detailRange], NSGrammarCorrections: detail[@"corrections"] ?: @[ ] }];
}
[resultsForWord addObject:adoptNS([[LayoutTestTextCheckingResult alloc] initWithType:nsTextCheckingType(type) range:NSMakeRange(from, to - from) replacement:replacement details:details.get()]).get()];
}
[finalResults setObject:resultsForWord.get() forKey:stringToCheck];
}
_results = WTF::move(finalResults);
}
#if PLATFORM(MAC)
static const char *stringForCorrectionResponse(NSCorrectionResponse correctionResponse)
{
switch (correctionResponse) {
case NSCorrectionResponseNone:
return "none";
case NSCorrectionResponseAccepted:
return "accepted";
case NSCorrectionResponseRejected:
return "rejected";
case NSCorrectionResponseIgnored:
return "ignored";
case NSCorrectionResponseEdited:
return "edited";
case NSCorrectionResponseReverted:
return "reverted";
}
return "invalid";
}
- (NSArray<NSTextCheckingResult *> *)checkString:(NSString *)stringToCheck range:(NSRange)range types:(NSTextCheckingTypes)checkingTypes options:(NSDictionary<NSString *, id> *)options inSpellDocumentWithTag:(NSInteger)tag orthography:(NSOrthography **)orthography wordCount:(NSInteger *)wordCount
{
NSArray *result = [super checkString:stringToCheck range:range types:checkingTypes options:options inSpellDocumentWithTag:tag orthography:orthography wordCount:wordCount];
if (auto *overrideResult = [_results objectForKey:stringToCheck])
return overrideResult;
return result;
}
- (void)recordResponse:(NSCorrectionResponse)response toCorrection:(NSString *)correction forWord:(NSString *)word language:(NSString *)language inSpellDocumentWithTag:(NSInteger)tag
{
if (_spellCheckerLoggingEnabled)
printf("NSSpellChecker recordResponseToCorrection: %s -> %s (response: %s)\n", [word UTF8String], [correction UTF8String], stringForCorrectionResponse(response));
[super recordResponse:response toCorrection:correction forWord:word language:language inSpellDocumentWithTag:tag];
}
- (NSInteger)requestCheckingOfString:(NSString *)stringToCheck range:(NSRange)range types:(NSTextCheckingTypes)checkingTypes options:(NSDictionary<NSString *, id> *)options inSpellDocumentWithTag:(NSInteger)tag completionHandler:(TextCheckingCompletionHandler)completionHandler
{
return [super requestCheckingOfString:stringToCheck range:range types:checkingTypes options:options inSpellDocumentWithTag:tag completionHandler:[overrideResult = retainPtr([_results objectForKey:stringToCheck]), completion = makeBlockPtr(completionHandler), stringToCheck = retainPtr(stringToCheck)] (NSInteger sequenceNumber, NSArray<NSTextCheckingResult *> *result, NSOrthography *orthography, NSInteger wordCount) {
if (overrideResult) {
completion(sequenceNumber, overrideResult.get(), orthography, wordCount);
return;
}
completion(sequenceNumber, result, orthography, wordCount);
}];
}
- (NSInteger)requestGrammarCheckingOfString:(NSString *)stringToCheck range:(NSRange)range language:(NSString *)language options:(NSDictionary<NSString *, id> *)options completionHandler:(void (^)(NSInteger sequenceNumber, NSArray<NSTextCheckingResult *> *results))completionHandler
{
if (RetainPtr overrideResult = [_results objectForKey:stringToCheck]) {
completionHandler(0, overrideResult.get());
return 0; // Not currently used, so return value doesn't matter. Should be the sequence number.
}
return [super requestGrammarCheckingOfString:stringToCheck range:range language:language options:options completionHandler:completionHandler];
}
#endif // PLATFORM(MAC)
#if PLATFORM(IOS_FAMILY) && ENABLE(POST_EDITING_GRAMMAR_CHECKING)
- (void)requestProofreadingReviewOfString:(NSString *)stringToCheck range:(NSRange)range language:(NSString *)language options:(NSDictionary<NSString *, id> *)options completionHandler:(void (^)(NSArray<NSTextCheckingResult *> *results))completionHandler
{
if (RetainPtr overrideResult = [_results objectForKey:stringToCheck])
completionHandler(overrideResult.get());
return [super requestProofreadingReviewOfString:stringToCheck range:range language:language options:options completionHandler:completionHandler];
}
static NSDictionary *swizzledGrammarDetailsForString(id, SEL, NSString *stringToCheck, NSRange range, NSString *language)
{
for (LayoutTestTextCheckingResult *result in [globalSpellChecker->_results objectForKey:stringToCheck]) {
if (!NSEqualRanges(result.range, range))
continue;
if (result.resultType != NSTextCheckingTypeGrammar)
continue;
if (NSDictionary *details = result.grammarDetails.firstObject)
return details;
}
return @{ };
}
- (NSArray<NSTextAlternatives *> *)grammarAlternativesForString:(NSString *)string
{
InstanceMethodSwizzler swizzler { self.postEditor.class, @selector(grammarDetailForString:range:language:), reinterpret_cast<IMP>(swizzledGrammarDetailsForString) };
return [super grammarAlternativesForString:string];
}
- (NSArray<NSTextCheckingResult *> *)checkString:(NSString *)stringToCheck range:(NSRange)range types:(NSTextCheckingTypes)checkingTypes languages:(NSArray<NSString *> *)languagesArray options:(NSDictionary<NSString *, id> *)options
{
if (auto *overrideResult = [_results objectForKey:stringToCheck])
return overrideResult;
return [super checkString:stringToCheck range:range types:checkingTypes languages:languagesArray options:options];
}
- (BOOL)_doneLoading
{
return YES;
}
#endif // PLATFORM(IOS_FAMILY) && ENABLE(POST_EDITING_GRAMMAR_CHECKING)
@end