blob: 153944590305b8b5a212b728452060fc6b9b3cfe [file] [log] [blame]
/*
* 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 "JSBasics.h"
#import <objc/runtime.h>
#import <wtf/Assertions.h>
#import <wtf/BlockPtr.h>
#if PLATFORM(MAC)
using TextCheckingCompletionHandler = void(^)(NSInteger, NSArray<NSTextCheckingResult *> *, NSOrthography *, NSInteger);
static RetainPtr<LayoutTestSpellChecker> globalSpellChecker;
static BOOL hasSwizzledLayoutTestSpellChecker = NO;
static IMP globallySwizzledSharedSpellCheckerImplementation;
static Method originalSharedSpellCheckerMethod;
static LayoutTestSpellChecker *ensureGlobalLayoutTestSpellChecker()
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
globalSpellChecker = adoptNS([[LayoutTestSpellChecker alloc] init]);
});
return globalSpellChecker.get();
}
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";
}
static NSTextCheckingType nsTextCheckingType(JSStringRef jsType)
{
auto cfType = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, jsType));
if (CFStringCompare(cfType.get(), CFSTR("orthography"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypeOrthography;
if (CFStringCompare(cfType.get(), CFSTR("spelling"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypeSpelling;
if (CFStringCompare(cfType.get(), CFSTR("grammar"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypeGrammar;
if (CFStringCompare(cfType.get(), CFSTR("date"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypeDate;
if (CFStringCompare(cfType.get(), CFSTR("address"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypeAddress;
if (CFStringCompare(cfType.get(), CFSTR("link"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypeLink;
if (CFStringCompare(cfType.get(), CFSTR("quote"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypeQuote;
if (CFStringCompare(cfType.get(), CFSTR("dash"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypeDash;
if (CFStringCompare(cfType.get(), CFSTR("replacement"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypeReplacement;
if (CFStringCompare(cfType.get(), CFSTR("correction"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypeCorrection;
if (CFStringCompare(cfType.get(), CFSTR("regular-expression"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypeRegularExpression;
if (CFStringCompare(cfType.get(), CFSTR("phone-number"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypePhoneNumber;
if (CFStringCompare(cfType.get(), CFSTR("transit-information"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
return NSTextCheckingTypeTransitInformation;
ASSERT_NOT_REACHED();
return NSTextCheckingTypeSpelling;
}
@interface LayoutTestTextCheckingResult : NSTextCheckingResult {
@private
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;
@end
@implementation LayoutTestTextCheckingResult
- (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=[%tu, %tu] replacement='%@'>", self.class, self, _type, _range.location, _range.location + _range.length, _replacement.get()];
}
@end
@implementation LayoutTestSpellChecker
@synthesize spellCheckerLoggingEnabled = _spellCheckerLoggingEnabled;
+ (instancetype)checker
{
auto *spellChecker = ensureGlobalLayoutTestSpellChecker();
if (hasSwizzledLayoutTestSpellChecker)
return spellChecker;
originalSharedSpellCheckerMethod = class_getClassMethod(objc_getMetaClass("NSSpellChecker"), @selector(sharedSpellChecker));
globallySwizzledSharedSpellCheckerImplementation = method_setImplementation(originalSharedSpellCheckerMethod, reinterpret_cast<IMP>(ensureGlobalLayoutTestSpellChecker));
hasSwizzledLayoutTestSpellChecker = YES;
return spellChecker;
}
+ (void)uninstallAndReset
{
[globalSpellChecker reset];
if (!hasSwizzledLayoutTestSpellChecker)
return;
method_setImplementation(originalSharedSpellCheckerMethod, globallySwizzledSharedSpellCheckerImplementation);
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)context
{
auto resultsObject = JSValueToObject(context, resultsValue, nullptr);
auto fromPropertyName = WTR::createJSString("from");
auto toPropertyName = WTR::createJSString("to");
auto typePropertyName = WTR::createJSString("type");
auto replacementPropertyName = WTR::createJSString("replacement");
auto detailsPropertyName = WTR::createJSString("details");
auto results = adoptNS([[NSMutableDictionary alloc] init]);
// FIXME: Using the Objective-C API would make this logic easier to follow.
auto properties = JSObjectCopyPropertyNames(context, resultsObject);
for (size_t index = 0; index < JSPropertyNameArrayGetCount(properties); ++index) {
JSStringRef textToCheck = JSPropertyNameArrayGetNameAtIndex(properties, index);
JSObjectRef resultsArray = JSValueToObject(context, JSObjectGetProperty(context, resultsObject, textToCheck, nullptr), nullptr);
auto resultsArrayPropertyNames = JSObjectCopyPropertyNames(context, resultsArray);
auto resultsForWord = adoptNS([[NSMutableArray alloc] init]);
for (size_t resultIndex = 0; resultIndex < JSPropertyNameArrayGetCount(resultsArrayPropertyNames); ++resultIndex) {
auto resultsObject = JSValueToObject(context, JSObjectGetPropertyAtIndex(context, resultsArray, resultIndex, nullptr), nullptr);
long fromValue = JSValueToNumber(context, JSObjectGetProperty(context, resultsObject, fromPropertyName.get(), nullptr), nullptr);
long toValue = JSValueToNumber(context, JSObjectGetProperty(context, resultsObject, toPropertyName.get(), nullptr), nullptr);
auto typeValue = WTR::createJSString(context, JSObjectGetProperty(context, resultsObject, typePropertyName.get(), nullptr));
auto replacementValue = JSObjectGetProperty(context, resultsObject, replacementPropertyName.get(), nullptr);
RetainPtr<CFStringRef> replacementText;
if (!JSValueIsUndefined(context, replacementValue))
replacementText = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, WTR::createJSString(context, replacementValue).get()));
auto details = adoptNS([[NSMutableArray alloc] init]);
auto detailsValue = JSObjectGetProperty(context, resultsObject, detailsPropertyName.get(), nullptr);
if (!JSValueIsUndefined(context, detailsValue)) {
auto detailsObject = JSValueToObject(context, detailsValue, nullptr);
auto detailsObjectProperties = JSObjectCopyPropertyNames(context, detailsObject);
for (size_t detailIndex = 0; detailIndex < JSPropertyNameArrayGetCount(detailsObjectProperties); ++detailIndex) {
auto detailObject = JSValueToObject(context, JSObjectGetPropertyAtIndex(context, detailsObject, detailIndex, nullptr), nullptr);
long from = lroundl(JSValueToNumber(context, JSObjectGetProperty(context, detailObject, fromPropertyName.get(), nullptr), nullptr));
long to = lroundl(JSValueToNumber(context, JSObjectGetProperty(context, detailObject, toPropertyName.get(), nullptr), nullptr));
[details addObject:@{ NSGrammarRange: [NSValue valueWithRange:NSMakeRange(from, to - from)] }];
}
JSPropertyNameArrayRelease(detailsObjectProperties);
}
[resultsForWord addObject:adoptNS([[LayoutTestTextCheckingResult alloc] initWithType:nsTextCheckingType(typeValue.get()) range:NSMakeRange(fromValue, toValue - fromValue) replacement:(__bridge NSString *)replacementText.get() details:details.get()]).get()];
}
auto cfTextToCheck = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, textToCheck));
[results setObject:resultsForWord.get() forKey:(__bridge NSString *)cfTextToCheck.get()];
JSPropertyNameArrayRelease(resultsArrayPropertyNames);
}
JSPropertyNameArrayRelease(properties);
_results = WTFMove(results);
}
- (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);
}];
}
@end
#endif // PLATFORM(MAC)