1/* 2 GSspell.m 3 4 GNUstep spell checker facility. 5 6 Copyright (C) 2001, 2010 Free Software Foundation, Inc. 7 8 Author: Gregory John Casamento <greg_casamento@yahoo.com> 9 Date: May 2001 10 11 Author: Wolfgang Lux <wolfgang.lux@gmail.com> 12 Date: January 2010 13 14 This file is part of the GNUstep Project 15 16 This program is free software; you can redistribute it and/or 17 modify it under the terms of the GNU General Public License 18 as published by the Free Software Foundation; either version 3 19 of the License, or (at your option) any later version. 20 21 This program is distributed in the hope that it will be useful, 22 but WITHOUT ANY WARRANTY; without even the implied warranty of 23 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 GNU General Public License for more details. 25 26 You should have received a copy of the GNU General Public 27 License along with this library; see the file COPYING. 28 If not, see <http://www.gnu.org/licenses/> or write to the 29 Free Software Foundation, 51 Franklin Street, Fifth Floor, 30 Boston, MA 02110-1301, USA. 31 32*/ 33 34// get the configuration. 35#include "config.h" 36#import <AppKit/AppKit.h> 37#import <Foundation/Foundation.h> 38 39#ifdef HAVE_ASPELL_H 40#import <GNUstepBase/GSLocale.h> 41#import <GNUstepBase/Unicode.h> 42#include <aspell.h> 43#endif 44 45// A minor category for NSData so that we can convert NSStrings 46// into data. 47@interface NSData (MethodsForSpellChecker) 48+ (id)dataWithString: (NSString *)string; 49@end 50 51@implementation NSData (MethodsForSpellChecker) 52+ (id)dataWithString: (NSString *)string 53{ 54 NSData *data = [NSData dataWithBytes: (char *)[string cString] 55 length: [string length]]; 56 return data; 57} 58@end 59 60// A category for NSBundle so that we can determine the languages 61// vended by a service bundle 62@interface NSBundle (MethodsForSpellChecker) 63- (NSArray *) serviceLanguages; 64@end 65 66@implementation NSBundle (MethodsForSpellChecker) 67- (NSArray *) serviceLanguages 68{ 69 NSDictionary *infoDict = [self infoDictionary]; 70 if ([infoDict isKindOfClass: [NSDictionary class]]) 71 { 72 NSArray *services = [infoDict objectForKey: @"NSServices"]; 73 if ([services isKindOfClass: [NSArray class]] && [services count] > 0) 74 { 75 NSDictionary *serviceDict = [services objectAtIndex: 0]; 76 if ([serviceDict isKindOfClass: [NSDictionary class]]) 77 { 78 NSArray *languages = [serviceDict objectForKey: @"NSLanguages"]; 79 if ([languages isKindOfClass: [NSArray class]]) 80 { 81 return languages; 82 } 83 } 84 } 85 } 86 return nil; 87} 88@end 89 90// The base class. Its spell checker just provides a dumb spell checker 91// for American English as fallback if aspell is not available. 92 93@interface GNUSpellChecker : NSObject 94- (BOOL) registerLanguagesWithServer: (NSSpellServer *)aServer; 95- (NSArray *) languages; 96@end 97 98@implementation GNUSpellChecker 99 100- (BOOL) registerLanguagesWithServer: (NSSpellServer *)aServer 101{ 102 BOOL success = NO; 103 NSEnumerator *langEnum; 104 NSString *language; 105 106 langEnum = [[self languages] objectEnumerator]; 107 while ((language = [langEnum nextObject]) != nil) 108 { 109 if ([aServer registerLanguage: language byVendor: @"GNU"]) 110 { 111 NSLog(@"Registered spell server for language %@", language); 112 success = YES; 113 } 114 else 115 { 116 NSLog(@"Could not register spell server for language %@", language); 117 } 118 } 119 return success; 120} 121 122- (NSArray *) languages 123{ 124 return [NSArray arrayWithObject: @"AmericanEnglish"]; 125} 126 127- (BOOL) createBundleAtPath: (NSString *)path languages: (NSArray *)languages 128{ 129 NSDictionary *infoDict, *serviceDict; 130 NSFileManager *fm = [NSFileManager defaultManager]; 131 NSString *execPath; 132 133 if ([fm fileExistsAtPath: path] && ![fm removeFileAtPath: path handler: nil]) 134 { 135 NSLog(@"cannot remove %@", path); 136 return NO; 137 } 138 139 path = [path stringByAppendingPathComponent: @"Resources"]; 140 if (![fm createDirectoryAtPath: path 141 withIntermediateDirectories: YES 142 attributes: nil 143 error: NULL]) 144 { 145 NSLog(@"cannot not create bundle directory %@", path); 146 return NO; 147 } 148 149 path = [path stringByAppendingPathComponent: @"Info-gnustep"]; 150 path = [path stringByAppendingPathExtension: @"plist"]; 151 152 /* FIXME Not sure if the executable path is needed in the service dictionary. 153 However, GSspellInfo.plist has it and so we include it here too. */ 154 execPath = [[NSBundle mainBundle] executablePath]; 155 serviceDict = 156 [NSDictionary dictionaryWithObjectsAndKeys: 157 execPath, @"NSExecutable", 158 languages, @"NSLanguages", 159 @"GNU", @"NSSpellChecker", 160 nil]; 161 infoDict = 162 [NSDictionary dictionaryWithObjectsAndKeys: 163 execPath, @"NSExecutable", 164 [NSArray arrayWithObject: serviceDict], @"NSServices", 165 nil]; 166 if (![infoDict writeToFile: path atomically: YES]) 167 { 168 NSLog(@"cannot save info dictionary to %@", path); 169 return NO; 170 } 171 return YES; 172} 173 174- (BOOL) removeBundleAtPath: (NSString *)path 175{ 176 NSFileManager *fm = [NSFileManager defaultManager]; 177 178 if (![fm fileExistsAtPath: path]) 179 { 180 return NO; 181 } 182 if (![fm removeFileAtPath: path handler: nil]) 183 { 184 NSLog(@"cannot remove %@", path); 185 return NO; 186 } 187 return YES; 188} 189 190/* The installed services bundle only vends a spelling service for the 191 AmericanEnglish language. In order to make other languages available, 192 we maintain a bundle in the user's Services directory that vends those 193 languages. The bundle shares our server executable through its info 194 dictionary. */ 195- (void) synchronizeLanguages 196{ 197 NSArray *paths; 198 NSString *path; 199 NSMutableArray *otherLanguages; 200 201 paths = 202 NSSearchPathForDirectoriesInDomains (NSLibraryDirectory, 203 NSUserDomainMask, 204 YES); 205 path = [paths objectAtIndex:0]; 206 path = [path stringByAppendingPathComponent: @"Services"]; 207 path = [path stringByAppendingPathComponent: @"GSspell"]; 208 path = [path stringByAppendingPathExtension: @"service"]; 209 210 otherLanguages = [[[self languages] mutableCopy] autorelease]; 211 [otherLanguages removeObject: @"AmericanEnglish"]; 212 [otherLanguages sortUsingSelector: @selector(compare:)]; 213 if ([otherLanguages count]) 214 { 215 if (![otherLanguages isEqual: 216 [[NSBundle bundleWithPath: path] serviceLanguages]]) 217 { 218 if ([self createBundleAtPath: path languages: otherLanguages]) 219 { 220 [[NSWorkspace sharedWorkspace] findApplications]; 221 } 222 } 223 } 224 else 225 { 226 if ([self removeBundleAtPath: path]) 227 { 228 [[NSWorkspace sharedWorkspace] findApplications]; 229 } 230 } 231} 232 233- (NSRange) spellServer: (NSSpellServer *)sender 234findMisspelledWordInString: (NSString *)stringToCheck 235 language: (NSString *)language 236 wordCount: (int *)wordCount 237 countOnly: (BOOL)countOnly 238{ 239 NSRange r = NSMakeRange(0,0); 240 241 if (countOnly) 242 { 243 NSScanner *inputScanner = [NSScanner scannerWithString: stringToCheck]; 244 [inputScanner setCharactersToBeSkipped: 245 [NSCharacterSet whitespaceAndNewlineCharacterSet]]; 246 while (![inputScanner isAtEnd]) 247 { 248 [inputScanner scanUpToCharactersFromSet: 249 [NSCharacterSet whitespaceAndNewlineCharacterSet] 250 intoString: NULL]; 251 (*wordCount)++; 252 } 253 } 254 else 255 { 256 NSLog(@"spellServer:findMisspelledWordInString:... invoked, " 257 @"spell server not configured."); 258 } 259 260 return r; 261} 262 263- (NSArray *) spellServer: (NSSpellServer *)sender 264 suggestGuessesForWord: (NSString *)word 265 inLanguage: (NSString *)language 266{ 267 NSMutableArray *array = [NSMutableArray array]; 268 269 NSLog(@"spellServer:suggestGuessesForWord:... invoked, " 270 @"spell server not configured"); 271 272 return array; 273} 274 275- (void) spellServer: (NSSpellServer *)sender 276 didLearnWord: (NSString *)word 277 inLanguage: (NSString *)language 278{ 279 NSLog(@"spellServer:didLearnWord:inLanguage: invoked, " 280 @"spell server not configured"); 281} 282 283- (void) spellServer: (NSSpellServer *)sender 284 didForgetWord: (NSString *)word 285 inLanguage: (NSString *)language 286{ 287 NSLog(@"spellServer:didForgetWord:inLanguage: invoked, " 288 @"spell server not configured"); 289} 290 291@end 292 293#ifdef HAVE_ASPELL_H 294 295// The real speller checker class provides spelling services for all 296// languages that aspell has dictionaries installed. 297 298#define GNU_SPELL_CHECKER_CLASS GNUAspellSpellChecker 299@interface GNUAspellSpellChecker : GNUSpellChecker 300{ 301 NSDictionary *dictionaries; 302 NSMutableDictionary *spellers, *documentCheckers; 303} 304@end 305 306@implementation GNUAspellSpellChecker 307 308static NSDictionary * 309aspell_dictionaries() 310{ 311 AspellConfig *config; 312 AspellDictInfoList *dictList; 313 AspellDictInfoEnumeration *dictEnum; 314 NSMutableDictionary *dictionaries; 315 316 config = new_aspell_config(); 317 dictList = get_aspell_dict_info_list(config); 318 delete_aspell_config(config); 319 320 dictionaries = [[NSMutableDictionary alloc] initWithCapacity: 1]; 321 dictEnum = aspell_dict_info_list_elements(dictList); 322 while (!aspell_dict_info_enumeration_at_end(dictEnum)) 323 { 324 const AspellDictInfo *dict = aspell_dict_info_enumeration_next(dictEnum); 325 /* The string encoding does not really matter here, since Aspell 326 represents dictionary languages by a two letter ISO 639 language 327 code followed by an optional two letter ISO 3166 country code, 328 all of which are plain ASCII characters. 329 Note that there may be multiple dictionaries for a language, 330 but we are interested only in the supported languages. 331 FIXME How can the user choose a particular dictionary variant 332 from the Spelling panel? */ 333 NSString *dictLang = [NSString stringWithUTF8String: dict->code]; 334 NSString *language = GSLanguageFromLocale(dictLang); 335 if (!language) 336 language = dictLang; 337 [dictionaries setObject: dictLang forKey: language]; 338 } 339 delete_aspell_dict_info_enumeration(dictEnum); 340 341 return dictionaries; 342} 343 344- (id) init 345{ 346 if (![super init]) 347 return nil; 348 349 dictionaries = aspell_dictionaries(); 350 spellers = [[NSMutableDictionary alloc] initWithCapacity: 1]; 351 documentCheckers = [[NSMutableDictionary alloc] initWithCapacity: 1]; 352 353 return self; 354} 355 356- (NSArray *) languages 357{ 358 return [dictionaries allKeys]; 359} 360 361- (AspellSpeller *) spellerForLanguage: (NSString *)language 362{ 363 AspellSpeller *speller = [[spellers objectForKey: language] pointerValue]; 364 if (!speller) 365 { 366 NSString *dictLang = [dictionaries objectForKey: language]; 367 if (dictLang) 368 { 369 AspellConfig *config = new_aspell_config(); 370 aspell_config_replace(config, "lang", [dictLang UTF8String]); 371 aspell_config_replace(config, "encoding", "UTF-8"); 372 speller = to_aspell_speller(new_aspell_speller(config)); 373 [spellers setObject: [NSValue valueWithPointer: speller] 374 forKey: language]; 375 } 376 } 377 return speller; 378} 379 380- (AspellDocumentChecker *) documentCheckerForLanguage: (NSString *)language 381{ 382 AspellDocumentChecker *checker = 383 [[documentCheckers objectForKey: language] pointerValue]; 384 if (!checker) 385 { 386 AspellSpeller *speller = [self spellerForLanguage: language]; 387 checker = 388 to_aspell_document_checker(new_aspell_document_checker(speller)); 389 [documentCheckers setObject: [NSValue valueWithPointer: checker] 390 forKey: language]; 391 } 392 return checker; 393} 394 395static inline unsigned int 396uniLength(unsigned char *buf, unsigned int len) 397{ 398 unsigned int i, size; 399 400 for (i = 0; i < len; i++) 401 { 402 if (buf[i] >= 0x80) 403 { 404 if (GSToUnicode(0, &size, buf, len, NSUTF8StringEncoding, 0, 0)) 405 { 406 len = size; 407 } 408 break; 409 } 410 } 411 return len; 412} 413 414- (NSRange) spellServer: (NSSpellServer *)sender 415findMisspelledWordInString: (NSString *)stringToCheck 416 language: (NSString *)language 417 wordCount: (int *)wordCount 418 countOnly: (BOOL)countOnly 419{ 420 const char *p; 421 AspellToken token; 422 AspellDocumentChecker *checker; 423 NSRange r; 424 NSString *word; 425 int length; 426 427 if (countOnly) 428 { 429 return [super spellServer: sender 430 findMisspelledWordInString: stringToCheck 431 language: language 432 wordCount: wordCount 433 countOnly: countOnly]; 434 } 435 436 p = [stringToCheck UTF8String]; 437 length = strlen(p); 438 439 checker = [self documentCheckerForLanguage: language]; 440 aspell_document_checker_process(checker, p, length); 441 442 /* Even though we add learned words to aspell's user dictionary, we must 443 ask the server for words in its user dictionaries so that words that 444 the user has ignored won't be returned as misspelled. */ 445 do 446 { 447 token = aspell_document_checker_next_misspelling(checker); 448 if (token.len == 0) 449 return NSMakeRange(NSNotFound, 0); 450 451 r = NSMakeRange(uniLength((unsigned char *)p, token.offset), 452 uniLength((unsigned char *)p + token.offset, token.len)); 453 word = [stringToCheck substringWithRange: r]; 454 } 455 while ([sender isWordInUserDictionaries: word caseSensitive: YES]); 456 457 return r; 458} 459 460- (NSArray *) spellServer: (NSSpellServer *)sender 461 suggestGuessesForWord: (NSString *)word 462 inLanguage: (NSString *)language 463{ 464 NSMutableArray *array = [NSMutableArray array]; 465 466 const char *p = [word UTF8String]; 467 int len = strlen(p); 468 int words = 0; 469 AspellSpeller *speller = [self spellerForLanguage: language]; 470 const struct AspellWordList *list = aspell_speller_suggest(speller, p, len); 471 AspellStringEnumeration *en; 472 473 words = aspell_word_list_size(list); 474 en = aspell_word_list_elements(list); 475 476 // add them to the array. 477 while (!aspell_string_enumeration_at_end(en)) 478 { 479 const char *string = aspell_string_enumeration_next(en); 480 NSString *word = [NSString stringWithUTF8String: string]; 481 [array addObject: word]; 482 } 483 484 // cleanup. 485 delete_aspell_string_enumeration(en); 486 487 return array; 488} 489 490- (void) spellServer: (NSSpellServer *)sender 491 didLearnWord: (NSString *)word 492 inLanguage: (NSString *)language 493{ 494 const char *aword = [word UTF8String]; 495 AspellSpeller *speller = [self spellerForLanguage: language]; 496 aspell_speller_add_to_personal(speller, aword, strlen(aword)); 497} 498 499- (void) spellServer: (NSSpellServer *)sender 500 didForgetWord: (NSString *)word 501 inLanguage: (NSString *)language 502{ 503 NSLog(@"Not implemented"); 504} 505 506@end 507 508#endif 509 510// The main program 511#ifndef GNU_SPELL_CHECKER_CLASS 512#define GNU_SPELL_CHECKER_CLASS GNUSpellChecker 513#endif 514 515#ifdef GNUSTEP 516int main(int argc, char** argv, char **env) 517#else 518int main(int argc, char** argv) 519#endif 520{ 521 CREATE_AUTORELEASE_POOL (_pool); 522 NSSpellServer *aServer = [[NSSpellServer alloc] init]; 523 GNUSpellChecker *aSpellChecker = [[GNU_SPELL_CHECKER_CLASS alloc] init]; 524 525 NSLog(@"NSLanguages = %@", [aSpellChecker languages]); 526 [aSpellChecker synchronizeLanguages]; 527 if ([aSpellChecker registerLanguagesWithServer: aServer]) 528 { 529 [aServer setDelegate: aSpellChecker]; 530 NSLog(@"Spell server started and waiting."); 531 [aServer run]; 532 NSLog(@"Unexpected death of spell checker"); 533 } 534 else 535 { 536 NSLog(@"Cannot create spell checker instance"); 537 } 538 RELEASE(aSpellChecker); 539 RELEASE(aServer); 540 [_pool drain]; 541 return 0; 542} 543