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