168 lines
6.0 KiB
Objective-C
168 lines
6.0 KiB
Objective-C
//
|
|
// KBSuggestionEngine.m
|
|
// CustomKeyboard
|
|
//
|
|
|
|
#import "KBSuggestionEngine.h"
|
|
#import "KBConfig.h"
|
|
|
|
@interface KBSuggestionEngine ()
|
|
@property (nonatomic, copy) NSArray<NSString *> *words;
|
|
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *selectionCounts;
|
|
@property (nonatomic, strong) NSSet<NSString *> *priorityWords;
|
|
@end
|
|
|
|
@implementation KBSuggestionEngine
|
|
|
|
+ (instancetype)shared {
|
|
static KBSuggestionEngine *engine;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
engine = [[KBSuggestionEngine alloc] init];
|
|
});
|
|
return engine;
|
|
}
|
|
|
|
- (instancetype)init {
|
|
if (self = [super init]) {
|
|
_selectionCounts = [NSMutableDictionary dictionary];
|
|
NSArray<NSString *> *defaults = [self.class kb_defaultWords];
|
|
_priorityWords = [NSSet setWithArray:defaults];
|
|
_words = [self kb_loadWords];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (NSArray<NSString *> *)suggestionsForPrefix:(NSString *)prefix limit:(NSUInteger)limit {
|
|
if (prefix.length == 0 || limit == 0) { return @[]; }
|
|
NSString *lower = prefix.lowercaseString;
|
|
NSMutableArray<NSString *> *matches = [NSMutableArray array];
|
|
|
|
for (NSString *word in self.words) {
|
|
if ([word hasPrefix:lower]) {
|
|
[matches addObject:word];
|
|
if (matches.count >= limit * 3) {
|
|
// Avoid scanning too many matches for long lists.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (matches.count == 0) { return @[]; }
|
|
|
|
[matches sortUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
|
|
NSInteger ca = self.selectionCounts[a].integerValue;
|
|
NSInteger cb = self.selectionCounts[b].integerValue;
|
|
if (ca != cb) {
|
|
return (cb > ca) ? NSOrderedAscending : NSOrderedDescending;
|
|
}
|
|
BOOL pa = [self.priorityWords containsObject:a];
|
|
BOOL pb = [self.priorityWords containsObject:b];
|
|
if (pa != pb) {
|
|
return pa ? NSOrderedAscending : NSOrderedDescending;
|
|
}
|
|
return [a compare:b];
|
|
}];
|
|
|
|
if (matches.count > limit) {
|
|
return [matches subarrayWithRange:NSMakeRange(0, limit)];
|
|
}
|
|
return matches.copy;
|
|
}
|
|
|
|
- (void)recordSelection:(NSString *)word {
|
|
if (word.length == 0) { return; }
|
|
NSString *key = word.lowercaseString;
|
|
NSInteger count = self.selectionCounts[key].integerValue + 1;
|
|
self.selectionCounts[key] = @(count);
|
|
}
|
|
|
|
#pragma mark - Defaults
|
|
|
|
- (NSArray<NSString *> *)kb_loadWords {
|
|
NSMutableOrderedSet<NSString *> *set = [[NSMutableOrderedSet alloc] init];
|
|
[set addObjectsFromArray:[self.class kb_defaultWords]];
|
|
|
|
NSArray<NSString *> *paths = [self kb_wordListPaths];
|
|
for (NSString *path in paths) {
|
|
if (path.length == 0) { continue; }
|
|
NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
|
|
if (content.length == 0) { continue; }
|
|
NSArray<NSString *> *lines = [content componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
|
|
for (NSString *line in lines) {
|
|
NSString *word = [self kb_sanitizedWordFromLine:line];
|
|
if (word.length == 0) { continue; }
|
|
[set addObject:word];
|
|
}
|
|
}
|
|
|
|
NSArray<NSString *> *result = set.array ?: @[];
|
|
return result;
|
|
}
|
|
|
|
- (NSArray<NSString *> *)kb_wordListPaths {
|
|
NSMutableArray<NSString *> *paths = [NSMutableArray array];
|
|
// 1) App Group override (allows server-downloaded large list).
|
|
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:AppGroup];
|
|
if (containerURL.path.length > 0) {
|
|
NSString *groupPath = [[containerURL path] stringByAppendingPathComponent:@"kb_words.txt"];
|
|
[paths addObject:groupPath];
|
|
}
|
|
// 2) Bundle fallback.
|
|
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"kb_words" ofType:@"txt"];
|
|
if (bundlePath.length > 0) {
|
|
[paths addObject:bundlePath];
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
- (NSString *)kb_sanitizedWordFromLine:(NSString *)line {
|
|
NSString *trimmed = [[line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];
|
|
if (trimmed.length == 0) { return @""; }
|
|
static NSCharacterSet *letters = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
letters = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyz"];
|
|
});
|
|
for (NSUInteger i = 0; i < trimmed.length; i++) {
|
|
if (![letters characterIsMember:[trimmed characterAtIndex:i]]) {
|
|
return @"";
|
|
}
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
+ (NSArray<NSString *> *)kb_defaultWords {
|
|
return @[
|
|
@"a", @"an", @"and", @"are", @"as", @"at",
|
|
@"app", @"ap", @"apple", @"apply", @"april", @"application",
|
|
@"about", @"above", @"after", @"again", @"against", @"all",
|
|
@"am", @"among", @"amount", @"any", @"around",
|
|
@"be", @"because", @"been", @"before", @"being", @"below",
|
|
@"best", @"between", @"both", @"but", @"by",
|
|
@"can", @"could", @"come", @"common", @"case",
|
|
@"do", @"does", @"down", @"day",
|
|
@"each", @"early", @"end", @"even", @"every",
|
|
@"for", @"from", @"first", @"found", @"free",
|
|
@"get", @"good", @"great", @"go",
|
|
@"have", @"has", @"had", @"help", @"how",
|
|
@"in", @"is", @"it", @"if", @"into",
|
|
@"just", @"keep", @"kind", @"know",
|
|
@"like", @"look", @"long", @"last",
|
|
@"make", @"more", @"most", @"my",
|
|
@"new", @"no", @"not", @"now",
|
|
@"of", @"on", @"one", @"or", @"other", @"our", @"out",
|
|
@"people", @"place", @"please",
|
|
@"quick", @"quite",
|
|
@"right", @"read", @"real",
|
|
@"see", @"say", @"some", @"such", @"so",
|
|
@"the", @"to", @"this", @"that", @"them", @"then", @"there", @"they", @"these", @"time",
|
|
@"use", @"up", @"under",
|
|
@"very",
|
|
@"we", @"with", @"what", @"when", @"where", @"who", @"why", @"will", @"would",
|
|
@"you", @"your"
|
|
];
|
|
}
|
|
|
|
@end
|