├── .gitignore ├── Makefile ├── README.md ├── asdf.conf ├── enchant.asd ├── enchant.lisp ├── print-doc.lisp └── readme.lisp /.gitignore: -------------------------------------------------------------------------------- 1 | quicklisp/ 2 | build/ 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | sbcl = $(shell which sbcl) 2 | 3 | README.md: quicklisp/setup.lisp enchant.lisp readme.lisp print-doc.lisp 4 | $(sbcl) --script readme.lisp >$@ 5 | 6 | quicklisp/install.lisp: 7 | mkdir -p quicklisp 8 | wget -O $@ "http://beta.quicklisp.org/quicklisp.lisp" 9 | 10 | quicklisp/setup.lisp: quicklisp/install.lisp 11 | $(sbcl) --noinform --no-sysinit --no-userinit --non-interactive \ 12 | --load asdf.conf \ 13 | --load quicklisp/install.lisp \ 14 | --eval '(quicklisp-quickstart:install :path "quicklisp/")' 15 | 16 | clean: 17 | rm -fr build 18 | 19 | distclean: clean 20 | rm -fr quicklisp 21 | 22 | .PHONY: clean distclean 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Enchant 2 | ======= 3 | 4 | **Common Lisp programming interface for Enchant spell-checker library** 5 | 6 | 7 | Introduction 8 | ------------ 9 | 10 | Enchant is a Common Lisp interface for the [Enchant][] spell-checker 11 | library. The Enchant library is a generic spell-checker library which 12 | uses other spell-checkers transparently as back-end. The library 13 | supports the following checkers: 14 | 15 | - Hunspell 16 | - GNU Aspell 17 | - Hspell 18 | - Voikko 19 | - Apple Spell 20 | - Zemberek 21 | 22 | This Common Lisp Enchant package uses [The Common Foreign Function 23 | Interface][CFFI] (CFFI) for accessing the Enchant C library. This 24 | package should work on any Common Lisp implementation which supports 25 | CFFI. 26 | 27 | [Enchant]: https://rrthomas.github.io/enchant/ 28 | [CFFI]: https://cffi.common-lisp.dev/ 29 | 30 | 31 | Installation 32 | ------------ 33 | 34 | [Quicklisp][QL] is the easiest way to install and load Enchant. There is 35 | one [ASDF][] system `enchant` which also loads the Enchant C library 36 | using CFFI's facilities. 37 | 38 | [QL]: https://www.quicklisp.org/ 39 | [ASDF]: https://asdf.common-lisp.dev/ 40 | 41 | 42 | Examples 43 | -------- 44 | 45 | ### Function: `(dict-check dict word)` 46 | 47 | Check the spelling for _word_ using dictionary _dict_. 48 | 49 | ENCHANT> (with-dict (lang "en_GB") 50 | (dict-check lang "working")) ; correct 51 | "working" 52 | 53 | ENCHANT> (with-dict (lang "en_GB") 54 | (dict-check lang "wrking")) ; incorrect 55 | NIL 56 | 57 | 58 | ### Function: `(dict-suggest dict word)` 59 | 60 | Get spelling suggestions for _word_ using dictionary _dict_. 61 | 62 | ENCHANT> (with-dict (lang "en_US") 63 | (dict-suggest lang "wrking")) 64 | ("wring" "working" "irking" "waking" "wrying" "parking" 65 | "marking" "winking" "wicking" "Zworykin" "dragging") 66 | 67 | 68 | License and Source Code 69 | ----------------------- 70 | 71 | Author: Teemu Likonen <> 72 | 73 | OpenPGP key: [6965F03973F0D4CA22B9410F0F2CAE0E07608462][PGP] 74 | 75 | License: [Creative Commons CC0][CC0] (public domain dedication) 76 | 77 | The source code repository: 78 | 79 | 80 | [PGP]: http://www.iki.fi/tlikonen/pgp-key.asc 81 | [CC0]: https://creativecommons.org/publicdomain/zero/1.0/legalcode 82 | 83 | 84 | The Programming Interface 85 | ------------------------- 86 | 87 | Enchant package uses similar names to the original Enchant C library. 88 | Common Lisp's naming conventions are respected, though. For example, the 89 | original C language function `enchant_dict_check()` has been named 90 | `enchant:dict-check`. The C function `enchant_broker_dict_exists()` has 91 | been named `enchant:broker-dict-exists-p` and thus respecting the Common 92 | Lisp suffix convention for predicate functions. 93 | 94 | There are also macros for convenience: `with-broker`, `with-dict` and 95 | `with-pwl-dict`. They hide some low-level resource initialization and 96 | freeing operations. 97 | 98 | 99 | ### Class: `broker` 100 | 101 | Class for holding pointers to foreign (non-Lisp) broker resources. 102 | Instances are created with `broker-init` function. 103 | 104 | 105 | ### Class: `dict` 106 | 107 | Class for holding pointers to foreign (non-Lisp) dictionary 108 | resources. Instances are created with `broker-request-dict` function. 109 | 110 | 111 | ### Function: `activep` 112 | 113 | The lambda list: 114 | 115 | (object) 116 | 117 | Test if _object_ is active. Return a generalized 118 | boolean. This can be used with `broker` and `dict` objects. 119 | 120 | 121 | ### Function: `broker-describe` 122 | 123 | The lambda list: 124 | 125 | (broker) 126 | 127 | Get information about Enchant dictionary providers. Return a list of 128 | lists of three strings: provider name, provider description, provider 129 | library filename. 130 | 131 | If _broker_ is not an active `broker` object, signal `not-active-broker` 132 | error condition. 133 | 134 | 135 | ### Function: `broker-dict-exists-p` 136 | 137 | The lambda list: 138 | 139 | (broker language) 140 | 141 | Check if dictionary for _language_ exists. _Broker_ must be a valid 142 | `broker` object returned by `broker-init`. _Language_ is a language code 143 | and optional country code as a string (e.g., `"en"`, `"en_GB"`). If 144 | the _language_ exists return the _language_ string. Otherwise return 145 | `nil`. 146 | 147 | If _broker_ is not an active `broker` object, signal `not-active-broker` 148 | error condition. 149 | 150 | 151 | ### Function: `broker-free` 152 | 153 | The lambda list: 154 | 155 | (broker) 156 | 157 | Free the foreign (non-Lisp) `broker` resources. The argument is a 158 | `broker` object returned by `broker-init`. The `broker` object becomes 159 | inactive and can't be used anymore. 160 | 161 | 162 | ### Function: `broker-free-dict` 163 | 164 | The lambda list: 165 | 166 | (broker dict) 167 | 168 | Free the foreign (non-Lisp) `dict` resources. The first argument is a 169 | `broker` object returned by `broker-init` and the second a `dict` object 170 | returned by `broker-request-dict`. The `dict` object becomes inactive 171 | and can't be used anymore. 172 | 173 | 174 | ### Function: `broker-get-error` 175 | 176 | The lambda list: 177 | 178 | (broker) 179 | 180 | Return an error message string (or `nil`) describing the last error 181 | in the UTF-8 encoding. This can be called after `broker` operations. 182 | 183 | 184 | ### Function: `broker-init` 185 | 186 | Initialize a new broker. Return a `broker` object which can be used 187 | to request dictionaries etc. See function `broker-request-dict`. 188 | 189 | A `broker` object is active when it has been successfully created. It 190 | allocates foreign (non-Lisp) resources and must be freed after use with 191 | function `broker-free`. After being freed it becomes inactive and thus 192 | unusable. Generic function `activep` can be used to test if a `broker` 193 | object is active or not. 194 | 195 | See macros `with-broker`, `with-dict` and `with-pwl-dict` which 196 | automatically initialize and free broker and dictionary resources. 197 | 198 | 199 | ### Function: `broker-list-dicts` 200 | 201 | The lambda list: 202 | 203 | (broker) 204 | 205 | List all dictionaries that are available. Return a list of lists with 206 | four strings: language and optional country code (e.g., `"en"` or 207 | `"en_GB"`), provider name, provider description and provider library 208 | filename. 209 | 210 | If _broker_ is not an active `broker` object, signal `not-active-broker` 211 | error condition. 212 | 213 | 214 | ### Function: `broker-request-dict` 215 | 216 | The lambda list: 217 | 218 | (broker language) 219 | 220 | Request a new dictionary for _language_. Return a `dict` object which 221 | can be used with spell-checker operations etc. 222 | 223 | The _broker_ argument must be an active `broker` object created with 224 | `broker-init`. _Language_ is a language code and optional country code 225 | as a string (e.g., `"en"` or `"en_GB"`). 226 | 227 | A `dict` object is active when it has been successfully created. It 228 | allocates foreign (non-Lisp) resources and must be freed after use with 229 | function `broker-free-dict`. After being freed it becomes inactive and 230 | thus unusable. Generic function `activep` can be used to test if `dict` 231 | object is active or not. 232 | 233 | If no suitable dictionary could be found `dict-not-found` error 234 | condition is signaled. 235 | 236 | See also `with-dict` macro which automatically creates a `dict` 237 | environment and frees it in the end. 238 | 239 | 240 | ### Function: `broker-request-pwl-dict` 241 | 242 | The lambda list: 243 | 244 | (broker pwl) 245 | 246 | Request a new dictionary for personal word list file _pwl_ (a filename string). 247 | Return a `dict` object which can be used with spell-checker operations. 248 | 249 | The _broker_ argument must be an active `broker` object created with 250 | `broker-init`. Personal word list file _pwl_ is a text file with one 251 | entry (a word) per line. If the file does not exist it is created. New 252 | words can be added to the personal word list file with function 253 | `dict-add`. 254 | 255 | A `dict` object is active when it has been successfully created. It 256 | allocates foreign (non-Lisp) resources and must be freed after use with 257 | function `broker-free-dict`. After being freed it becomes inactive and 258 | thus unusable. Generic function `activep` can be used to test if `dict` 259 | object is active or not. 260 | 261 | See also `with-pwl-dict` macro which automatically creates a `dict` 262 | environment and frees it in the end. 263 | 264 | 265 | ### Function: `broker-set-ordering` 266 | 267 | The lambda list: 268 | 269 | (broker language ordering) 270 | 271 | Declares a preference of providers to use for _language_. 272 | The _language_ argument is a language code and optional country 273 | code (e.g., `"en"` or `"en_GB"`). Pseudo language `"*"` can be 274 | used to declare a default ordering. It is used by any language that does 275 | not explicitly declare an ordering. The _ordering_ argument is a list of 276 | provider name strings (e.g., `("myspell" "aspell" "ispell")`). 277 | 278 | If _broker_ is not an active `broker` object, signal `not-active-broker` 279 | error condition. 280 | 281 | 282 | ### Function: `dict-add` 283 | 284 | The lambda list: 285 | 286 | (dict word) 287 | 288 | Add _word_ to user's personal dictionary _dict_. If the _word_ exists 289 | in user's exclude dictionary also remove it from there. 290 | 291 | 292 | ### Function: `dict-add-to-session` 293 | 294 | The lambda list: 295 | 296 | (dict word) 297 | 298 | Add _word_ to the current spell-checking session _dict_. _Word_ is 299 | then recognized as a correct word in the current session. 300 | 301 | 302 | ### Function: `dict-check` 303 | 304 | The lambda list: 305 | 306 | (dict word) 307 | 308 | Check the spelling of _word_ (string) using dictionary _dict_. 309 | Return the _word_ if the spelling is correct, `nil` otherwise. 310 | 311 | _Dict_ must be an active `dict` object returned by 312 | `broker-request-dict`, if not, signal a `not-active-dict` condition. 313 | 314 | 315 | ### Function: `dict-describe` 316 | 317 | The lambda list: 318 | 319 | (dict) 320 | 321 | Describe dictionary _dict_. Return a list of four strings: language 322 | code and optional country code (e.g., `"en"` or `"en_GB"`), provider 323 | name, provider description and provider library filename. 324 | 325 | _Dict_ must be an active `dict` object returned by 326 | `broker-request-dict`, if not, signal a `not-active-dict` condition. 327 | 328 | 329 | ### Function: `dict-get-error` 330 | 331 | The lambda list: 332 | 333 | (dict) 334 | 335 | Return an error message string (or `nil`) describing the last error 336 | in the UTF-8 encoding. This can be called after `dict` operations. 337 | 338 | 339 | ### Function: `dict-is-added-p` 340 | 341 | The lambda list: 342 | 343 | (dict word) 344 | 345 | Check if _word_ has been added to user's personal dictionary or to 346 | the current spell-checking session _dict_. If true, return _word_. 347 | Otherwise return `nil`. 348 | 349 | Functions for adding words are `dict-add` and `dict-add-to-session`. 350 | 351 | 352 | ### Function: `dict-is-removed-p` 353 | 354 | The lambda list: 355 | 356 | (dict word) 357 | 358 | Check if _word_ has been removed from user's personal dictionary or 359 | from the current spell-checking session _dict_. If true, return _word_. 360 | Otherwise return `nil`. 361 | 362 | 363 | ### Function: `dict-remove` 364 | 365 | The lambda list: 366 | 367 | (dict word) 368 | 369 | Add _word_ to user's exclude dictionary for _dict_. If the _word_ 370 | exists in user's personal dictionary also remove it from there. 371 | 372 | 373 | ### Function: `dict-remove-from-session` 374 | 375 | The lambda list: 376 | 377 | (dict word) 378 | 379 | Remove _word_ from the current spell-checking session _dict_. 380 | The _word_ is not recognized anymore in the current session. 381 | 382 | 383 | ### Function: `dict-store-replacement` 384 | 385 | The lambda list: 386 | 387 | (dict word correction) 388 | 389 | Add a correction statement from misspelled _word_ to _correction_ 390 | using dictionary _dict_. _Correction_ might show up in the suggestion 391 | list. 392 | 393 | 394 | ### Function: `dict-suggest` 395 | 396 | The lambda list: 397 | 398 | (dict word) 399 | 400 | Request spelling suggestions for _word_ (string) using dictionary _dict_. 401 | Return a list of suggestions (strings) or `nil` if there aren't any. 402 | 403 | _Dict_ must be an active `dict` object returned by 404 | `broker-request-dict`, if not, signal `not-active-dict` condition. 405 | 406 | 407 | ### Function: `get-version` 408 | 409 | Return the Enchant library version. 410 | 411 | 412 | ### Macro: `with-broker` 413 | 414 | The lambda list: 415 | 416 | (variable &body body) 417 | 418 | Initialize a new `broker` (using `broker-init`) and lexically bind 419 | _variable_ to the `broker` object. Execute all _body_ forms and return 420 | the values of the last _body_ form. Finally, free the `broker` resources 421 | with function `broker-free`. 422 | 423 | 424 | ### Macro: `with-dict` 425 | 426 | The lambda list: 427 | 428 | ((variable language &optional broker) &body body) 429 | 430 | Request a new `dict` object for _language_. Lexically bind _variable_ 431 | to the new `dict` object and execute all _body_ forms. Return the values 432 | of the last _body_ form. Finally, free the `dict` resources with 433 | function `broker-free-dict`. 434 | 435 | If the optional _broker_ argument is given reuse that broker object when 436 | requesting `dict`. If the _broker_ argument is not given create 437 | implicitly a new `broker` object with `broker-init` and free it in the 438 | end with `broker-free`. Note that the decision about the _broker_ 439 | argument is done at the macro-expansion time. If there is 440 | anything (except the symbol `nil`) in the place of the _broker_ argument 441 | that will be used as the broker. 442 | 443 | Examples: 444 | 445 | ENCHANT> (with-dict (lang "en_GB") 446 | (dict-check lang "working")) 447 | "working" 448 | 449 | ENCHANT> (with-dict (lang "en_GB") 450 | (dict-suggest lang "wrking")) 451 | ("wring" "working" "irking" "waking" "wrecking" "winking" 452 | "wrinkling" "marking" "Wrekin" "raking") 453 | 454 | 455 | ### Macro: `with-pwl-dict` 456 | 457 | The lambda list: 458 | 459 | ((variable pwl &optional broker) &body body) 460 | 461 | Request a new `dict` object for personal word list file _pwl_. 462 | Lexically bind _variable_ to the new `dict` object and execute all 463 | _body_ forms. Return the values of the last _body_ form. Finally, free 464 | the `dict` resources with function `broker-free-dict`. 465 | 466 | For more information on personal word list files see the documentation 467 | of function `broker-request-pwl-dict`. 468 | 469 | If the optional _broker_ argument is given reuse that broker object when 470 | requesting `dict`. If the _broker_ argument is not given create 471 | implicitly a new `broker` object with `broker-init` and free it in the 472 | end with `broker-free`. Note that the decision about the _broker_ 473 | argument is done at the macro-expansion time. If there is 474 | anything (except the symbol `nil`) in the place of the _broker_ argument 475 | that will be used as the broker. 476 | 477 | 478 | -------------------------------------------------------------------------------- /asdf.conf: -------------------------------------------------------------------------------- 1 | ;; -*- mode:lisp -*- 2 | 3 | (require "asdf") 4 | 5 | (asdf:initialize-output-translations 6 | (list :output-translations 7 | :ignore-inherited-configuration 8 | (list (merge-pathnames "**/*.*") 9 | (merge-pathnames "build/**/*.*")))) 10 | 11 | (asdf:initialize-source-registry 12 | (list :source-registry 13 | :ignore-inherited-configuration 14 | (list :directory (merge-pathnames "src/")) 15 | (list :tree (merge-pathnames "quicklisp/dists/")))) 16 | -------------------------------------------------------------------------------- /enchant.asd: -------------------------------------------------------------------------------- 1 | (defsystem "enchant" 2 | :description "Programming interface for Enchant spell-checker library" 3 | :author "Teemu Likonen " 4 | :licence "Creative Commons CC0 (public domain dedication)" 5 | :depends-on ("cffi") 6 | :components ((:file "enchant"))) 7 | -------------------------------------------------------------------------------- /enchant.lisp: -------------------------------------------------------------------------------- 1 | ;;;; Enchant -- Programming interface for Enchant spell-checker library 2 | ;; 3 | ;; Author: Teemu Likonen 4 | ;; 5 | ;; License: Creative Commons CC0 (public domain dedication) 6 | ;; https://creativecommons.org/publicdomain/zero/1.0/legalcode 7 | 8 | (defpackage #:enchant 9 | (:use #:cl) 10 | (:export #:get-version #:enchant-error #:error-string #:activep 11 | 12 | #:broker #:broker-init #:not-active-broker #:broker-get-error 13 | #:broker-free #:with-broker #:broker-describe #:broker-list-dicts 14 | #:broker-set-ordering 15 | 16 | #:dict #:not-active-dict #:dict-not-found #:dict-get-error 17 | #:broker-request-dict #:broker-request-pwl-dict 18 | #:broker-free-dict #:dict-check #:broker-dict-exists-p #:with-dict 19 | #:with-pwl-dict #:dict-suggest 20 | 21 | #:dict-add #:dict-add-to-session #:dict-is-added-p 22 | #:dict-remove #:dict-remove-from-session #:dict-is-removed-p 23 | #:dict-store-replacement #:dict-describe)) 24 | 25 | (in-package #:enchant) 26 | 27 | (eval-when (:load-toplevel :execute) 28 | (with-simple-restart (skip-load-enchant "Skip loading Enchant C library.") 29 | (cffi:load-foreign-library '(:or 30 | (:default "libenchant-2") 31 | (:default "libenchant"))))) 32 | 33 | ;;; General 34 | 35 | (defun error-if-not-proper-string (object) 36 | (assert (and (stringp object) 37 | (plusp (length object))) 38 | nil "The argument must be a non-empty string.")) 39 | 40 | (define-condition enchant-error (error) 41 | ((error-string :initarg :string :reader error-string)) 42 | (:report (lambda (condition stream) 43 | (format stream "~A" (error-string condition))))) 44 | 45 | (defclass foreign-object () 46 | ((address :initarg :address :accessor address))) 47 | 48 | (defun proper-pointer-p (object) 49 | (and (cffi:pointerp object) 50 | (not (cffi:null-pointer-p object)))) 51 | 52 | (defgeneric activep (object) 53 | (:documentation "Test if _object_ is active. Return a generalized 54 | boolean. This can be used with `broker` and `dict` objects.")) 55 | 56 | (defmethod activep ((object foreign-object)) 57 | (proper-pointer-p (address object))) 58 | 59 | (defmethod print-object ((object foreign-object) stream) 60 | (print-unreadable-object (object stream :type t :identity t) 61 | (format stream "(~A)" (if (activep object) "ACTIVE" "INACTIVE")))) 62 | 63 | (defgeneric free-foreign-resource (object &key &allow-other-keys)) 64 | 65 | (defmethod free-foreign-resource :around ((object foreign-object) &key) 66 | (when (activep object) 67 | (call-next-method) 68 | (setf (address object) nil) 69 | object)) 70 | 71 | (defun get-version () 72 | "Return the Enchant library version." 73 | (cffi:foreign-funcall "enchant_get_version" :string)) 74 | 75 | (defvar *callback-data*) 76 | 77 | (cffi:defcallback broker-describe-fn :void ((name :string) 78 | (desc :string) 79 | (file :string)) 80 | (push (list name desc file) *callback-data*)) 81 | 82 | (cffi:defcallback dict-describe-fn :void ((lang :string) 83 | (name :string) 84 | (desc :string) 85 | (file :string)) 86 | (push (list lang name desc file) *callback-data*)) 87 | 88 | ;;; Brokers 89 | 90 | (defclass broker (foreign-object) 91 | nil 92 | (:documentation 93 | "Class for holding pointers to foreign (non-Lisp) broker resources. 94 | Instances are created with `broker-init` function.")) 95 | 96 | (define-condition not-active-broker (enchant-error) nil) 97 | 98 | (defun broker-init () 99 | "Initialize a new broker. Return a `broker` object which can be used 100 | to request dictionaries etc. See function `broker-request-dict`. 101 | 102 | A `broker` object is active when it has been successfully created. It 103 | allocates foreign (non-Lisp) resources and must be freed after use with 104 | function `broker-free`. After being freed it becomes inactive and thus 105 | unusable. Generic function `activep` can be used to test if a `broker` 106 | object is active or not. 107 | 108 | See macros `with-broker`, `with-dict` and `with-pwl-dict` which 109 | automatically initialize and free broker and dictionary resources." 110 | 111 | (let ((broker (cffi:foreign-funcall "enchant_broker_init" :pointer))) 112 | (when (proper-pointer-p broker) 113 | (make-instance 'broker :address broker)))) 114 | 115 | (defmethod free-foreign-resource ((broker broker) &key) 116 | (cffi:foreign-funcall "enchant_broker_free" 117 | :pointer (address broker) 118 | :void)) 119 | 120 | (defun broker-free (broker) 121 | "Free the foreign (non-Lisp) `broker` resources. The argument is a 122 | `broker` object returned by `broker-init`. The `broker` object becomes 123 | inactive and can't be used anymore." 124 | (free-foreign-resource broker)) 125 | 126 | (defun error-if-not-active-broker (object) 127 | (unless (and (typep object 'broker) 128 | (activep object)) 129 | (error 'not-active-broker :string "Not an active BROKER object."))) 130 | 131 | (defun broker-dict-exists-p (broker language) 132 | "Check if dictionary for _language_ exists. _Broker_ must be a valid 133 | `broker` object returned by `broker-init`. _Language_ is a language code 134 | and optional country code as a string (e.g., `\"en\"`, `\"en_GB\"`). If 135 | the _language_ exists return the _language_ string. Otherwise return 136 | `nil`. 137 | 138 | If _broker_ is not an active `broker` object, signal `not-active-broker` 139 | error condition." 140 | 141 | (error-if-not-active-broker broker) 142 | (error-if-not-proper-string language) 143 | (case (cffi:foreign-funcall "enchant_broker_dict_exists" 144 | :pointer (address broker) 145 | :string language 146 | :int) 147 | (0 nil) 148 | (1 language))) 149 | 150 | (defmacro with-broker (variable &body body) 151 | "Initialize a new `broker` (using `broker-init`) and lexically bind 152 | _variable_ to the `broker` object. Execute all _body_ forms and return 153 | the values of the last _body_ form. Finally, free the `broker` resources 154 | with function `broker-free`." 155 | 156 | (let ((broker (gensym "BROKER"))) 157 | `(let* ((,broker (broker-init)) 158 | (,variable ,broker)) 159 | (declare (ignorable ,variable)) 160 | (unwind-protect (progn ,@body) 161 | (broker-free ,broker))))) 162 | 163 | (defun broker-describe (broker) 164 | "Get information about Enchant dictionary providers. Return a list of 165 | lists of three strings: provider name, provider description, provider 166 | library filename. 167 | 168 | If _broker_ is not an active `broker` object, signal `not-active-broker` 169 | error condition." 170 | 171 | (error-if-not-active-broker broker) 172 | (let (*callback-data*) 173 | (cffi:foreign-funcall "enchant_broker_describe" 174 | :pointer (address broker) 175 | :pointer (cffi:callback broker-describe-fn) 176 | :pointer (cffi:null-pointer) 177 | :void) 178 | (nreverse *callback-data*))) 179 | 180 | (defun broker-list-dicts (broker) 181 | "List all dictionaries that are available. Return a list of lists with 182 | four strings: language and optional country code (e.g., `\"en\"` or 183 | `\"en_GB\"`), provider name, provider description and provider library 184 | filename. 185 | 186 | If _broker_ is not an active `broker` object, signal `not-active-broker` 187 | error condition." 188 | 189 | (error-if-not-active-broker broker) 190 | (let (*callback-data*) 191 | (cffi:foreign-funcall "enchant_broker_list_dicts" 192 | :pointer (address broker) 193 | :pointer (cffi:callback dict-describe-fn) 194 | :pointer (cffi:null-pointer) 195 | :void) 196 | (nreverse *callback-data*))) 197 | 198 | (defun broker-get-error (broker) 199 | "Return an error message string (or `nil`) describing the last error 200 | in the UTF-8 encoding. This can be called after `broker` operations." 201 | (error-if-not-active-broker broker) 202 | (cffi:foreign-funcall "enchant_broker_get_error" 203 | :pointer (address broker) 204 | :string)) 205 | 206 | (defun broker-set-ordering (broker language ordering) 207 | "Declares a preference of providers to use for _language_. 208 | The _language_ argument is a language code and optional country 209 | code (e.g., `\"en\"` or `\"en_GB\"`). Pseudo language `\"*\"` can be 210 | used to declare a default ordering. It is used by any language that does 211 | not explicitly declare an ordering. The _ordering_ argument is a list of 212 | provider name strings (e.g., `(\"myspell\" \"aspell\" \"ispell\")`). 213 | 214 | If _broker_ is not an active `broker` object, signal `not-active-broker` 215 | error condition." 216 | (error-if-not-active-broker broker) 217 | (error-if-not-proper-string language) 218 | (assert (and (consp ordering) 219 | (every #'stringp ordering)) 220 | nil "The ORDERING argument must be a list of strings.") 221 | (cffi:foreign-funcall "enchant_broker_set_ordering" 222 | :pointer (address broker) 223 | :string language 224 | :string (format nil "~{~A~^,~}" ordering) 225 | :void)) 226 | 227 | ;;; Dicts 228 | 229 | (defclass dict (foreign-object) 230 | nil 231 | (:documentation 232 | "Class for holding pointers to foreign (non-Lisp) dictionary 233 | resources. Instances are created with `broker-request-dict` function.")) 234 | 235 | (define-condition not-active-dict (enchant-error) nil) 236 | (define-condition dict-not-found (enchant-error) nil) 237 | 238 | (defun error-if-not-active-dict (object) 239 | (unless (and (typep object 'dict) (activep object)) 240 | (error 'not-active-dict :string "Not an active DICT object."))) 241 | 242 | (defun dict-get-error (dict) 243 | "Return an error message string (or `nil`) describing the last error 244 | in the UTF-8 encoding. This can be called after `dict` operations." 245 | (error-if-not-active-dict dict) 246 | (cffi:foreign-funcall "enchant_dict_get_error" 247 | :pointer (address dict) 248 | :string)) 249 | 250 | (defun broker-request-dict (broker language) 251 | "Request a new dictionary for _language_. Return a `dict` object which 252 | can be used with spell-checker operations etc. 253 | 254 | The _broker_ argument must be an active `broker` object created with 255 | `broker-init`. _Language_ is a language code and optional country code 256 | as a string (e.g., `\"en\"` or `\"en_GB\"`). 257 | 258 | A `dict` object is active when it has been successfully created. It 259 | allocates foreign (non-Lisp) resources and must be freed after use with 260 | function `broker-free-dict`. After being freed it becomes inactive and 261 | thus unusable. Generic function `activep` can be used to test if `dict` 262 | object is active or not. 263 | 264 | If no suitable dictionary could be found `dict-not-found` error 265 | condition is signaled. 266 | 267 | See also `with-dict` macro which automatically creates a `dict` 268 | environment and frees it in the end." 269 | 270 | (error-if-not-active-broker broker) 271 | (error-if-not-proper-string language) 272 | (let ((ptr (cffi:foreign-funcall "enchant_broker_request_dict" 273 | :pointer (address broker) 274 | :string language 275 | :pointer))) 276 | (if (proper-pointer-p ptr) 277 | (make-instance 'dict :address ptr) 278 | (error 'dict-not-found :string (format nil "Dictionary \"~A\" not found." 279 | language))))) 280 | 281 | 282 | (defun broker-request-pwl-dict (broker pwl) 283 | "Request a new dictionary for personal word list file _pwl_ (a filename string). 284 | Return a `dict` object which can be used with spell-checker operations. 285 | 286 | The _broker_ argument must be an active `broker` object created with 287 | `broker-init`. Personal word list file _pwl_ is a text file with one 288 | entry (a word) per line. If the file does not exist it is created. New 289 | words can be added to the personal word list file with function 290 | `dict-add`. 291 | 292 | A `dict` object is active when it has been successfully created. It 293 | allocates foreign (non-Lisp) resources and must be freed after use with 294 | function `broker-free-dict`. After being freed it becomes inactive and 295 | thus unusable. Generic function `activep` can be used to test if `dict` 296 | object is active or not. 297 | 298 | See also `with-pwl-dict` macro which automatically creates a `dict` 299 | environment and frees it in the end." 300 | 301 | (error-if-not-active-broker broker) 302 | (error-if-not-proper-string pwl) 303 | (let ((ptr (cffi:foreign-funcall "enchant_broker_request_pwl_dict" 304 | :pointer (address broker) 305 | :string pwl 306 | :pointer))) 307 | (when (proper-pointer-p ptr) 308 | (make-instance 'dict :address ptr)))) 309 | 310 | 311 | (defmethod free-foreign-resource ((dict dict) &key broker) 312 | (cffi:foreign-funcall "enchant_broker_free_dict" 313 | :pointer (address broker) 314 | :pointer (address dict) 315 | :void)) 316 | 317 | (defun broker-free-dict (broker dict) 318 | "Free the foreign (non-Lisp) `dict` resources. The first argument is a 319 | `broker` object returned by `broker-init` and the second a `dict` object 320 | returned by `broker-request-dict`. The `dict` object becomes inactive 321 | and can't be used anymore." 322 | (error-if-not-active-broker broker) 323 | (free-foreign-resource dict :broker broker)) 324 | 325 | (defun dict-check (dict word) 326 | "Check the spelling of _word_ (string) using dictionary _dict_. 327 | Return the _word_ if the spelling is correct, `nil` otherwise. 328 | 329 | _Dict_ must be an active `dict` object returned by 330 | `broker-request-dict`, if not, signal a `not-active-dict` condition." 331 | 332 | (error-if-not-active-dict dict) 333 | (error-if-not-proper-string word) 334 | (let ((value (cffi:foreign-funcall "enchant_dict_check" 335 | :pointer (address dict) 336 | :string word 337 | :int -1 ;use strlen() 338 | :int))) 339 | (cond ((zerop value) word) 340 | ((plusp value) nil) 341 | ((minusp value) 342 | (error 'enchant-error 343 | :string (format nil "Error code: ~A" value)))))) 344 | 345 | (defun dict-suggest (dict word) 346 | "Request spelling suggestions for _word_ (string) using dictionary _dict_. 347 | Return a list of suggestions (strings) or `nil` if there aren't any. 348 | 349 | _Dict_ must be an active `dict` object returned by 350 | `broker-request-dict`, if not, signal `not-active-dict` condition." 351 | 352 | (error-if-not-active-dict dict) 353 | (error-if-not-proper-string word) 354 | (cffi:with-foreign-object (len :int) 355 | (let ((suggestions (cffi:foreign-funcall "enchant_dict_suggest" 356 | :pointer (address dict) 357 | :string word 358 | :int -1 359 | :pointer len 360 | :pointer))) 361 | (when (proper-pointer-p suggestions) 362 | (unwind-protect (loop :for i :upfrom 0 :below (cffi:mem-aref len :int) 363 | :collect (cffi:mem-aref suggestions :string i)) 364 | (cffi:foreign-funcall "enchant_dict_free_string_list" 365 | :pointer (address dict) 366 | :pointer suggestions 367 | :void)))))) 368 | 369 | (defmacro with-dict ((variable language &optional broker) &body body) 370 | "Request a new `dict` object for _language_. Lexically bind _variable_ 371 | to the new `dict` object and execute all _body_ forms. Return the values 372 | of the last _body_ form. Finally, free the `dict` resources with 373 | function `broker-free-dict`. 374 | 375 | If the optional _broker_ argument is given reuse that broker object when 376 | requesting `dict`. If the _broker_ argument is not given create 377 | implicitly a new `broker` object with `broker-init` and free it in the 378 | end with `broker-free`. Note that the decision about the _broker_ 379 | argument is done at the macro-expansion time. If there is 380 | anything (except the symbol `nil`) in the place of the _broker_ argument 381 | that will be used as the broker. 382 | 383 | Examples: 384 | 385 | ENCHANT> (with-dict (lang \"en_GB\") 386 | (dict-check lang \"working\")) 387 | \"working\" 388 | 389 | ENCHANT> (with-dict (lang \"en_GB\") 390 | (dict-suggest lang \"wrking\")) 391 | (\"wring\" \"working\" \"irking\" \"waking\" \"wrecking\" \"winking\" 392 | \"wrinkling\" \"marking\" \"Wrekin\" \"raking\")" 393 | 394 | (let* ((brokersym (gensym "BROKER")) 395 | (dictsym (gensym "DICT")) 396 | (code `(let* ((,dictsym (broker-request-dict ,brokersym ,language)) 397 | (,variable ,dictsym)) 398 | (declare (ignorable ,variable)) 399 | (unwind-protect (progn ,@body) 400 | (broker-free-dict ,brokersym ,dictsym))))) 401 | (if broker 402 | `(let ((,brokersym ,broker)) ,code) 403 | `(with-broker ,brokersym ,code)))) 404 | 405 | (defmacro with-pwl-dict ((variable pwl &optional broker) &body body) 406 | "Request a new `dict` object for personal word list file _pwl_. 407 | Lexically bind _variable_ to the new `dict` object and execute all 408 | _body_ forms. Return the values of the last _body_ form. Finally, free 409 | the `dict` resources with function `broker-free-dict`. 410 | 411 | For more information on personal word list files see the documentation 412 | of function `broker-request-pwl-dict`. 413 | 414 | If the optional _broker_ argument is given reuse that broker object when 415 | requesting `dict`. If the _broker_ argument is not given create 416 | implicitly a new `broker` object with `broker-init` and free it in the 417 | end with `broker-free`. Note that the decision about the _broker_ 418 | argument is done at the macro-expansion time. If there is 419 | anything (except the symbol `nil`) in the place of the _broker_ argument 420 | that will be used as the broker." 421 | 422 | (let* ((brokersym (gensym "BROKER")) 423 | (dictsym (gensym "DICT")) 424 | (code `(let* ((,dictsym (broker-request-pwl-dict ,brokersym ,pwl)) 425 | (,variable ,dictsym)) 426 | (declare (ignorable ,variable)) 427 | (unwind-protect (progn ,@body) 428 | (broker-free-dict ,brokersym ,dictsym))))) 429 | (if broker 430 | `(let ((,brokersym ,broker)) ,code) 431 | `(with-broker ,brokersym ,code)))) 432 | 433 | 434 | (defun dict-add (dict word) 435 | "Add _word_ to user's personal dictionary _dict_. If the _word_ exists 436 | in user's exclude dictionary also remove it from there." 437 | (error-if-not-active-dict dict) 438 | (error-if-not-proper-string word) 439 | (cffi:foreign-funcall "enchant_dict_add" 440 | :pointer (address dict) 441 | :string word 442 | :int -1 443 | :void)) 444 | 445 | (defun dict-add-to-session (dict word) 446 | "Add _word_ to the current spell-checking session _dict_. _Word_ is 447 | then recognized as a correct word in the current session." 448 | (error-if-not-active-dict dict) 449 | (error-if-not-proper-string word) 450 | (cffi:foreign-funcall "enchant_dict_add_to_session" 451 | :pointer (address dict) 452 | :string word 453 | :int -1 454 | :void)) 455 | 456 | (defun dict-is-added-p (dict word) 457 | "Check if _word_ has been added to user's personal dictionary or to 458 | the current spell-checking session _dict_. If true, return _word_. 459 | Otherwise return `nil`. 460 | 461 | Functions for adding words are `dict-add` and `dict-add-to-session`." 462 | (error-if-not-active-dict dict) 463 | (error-if-not-proper-string word) 464 | (case (cffi:foreign-funcall "enchant_dict_is_added" 465 | :pointer (address dict) 466 | :string word 467 | :int -1 468 | :int) 469 | (1 word) 470 | (0 nil))) 471 | 472 | (defun dict-remove (dict word) 473 | "Add _word_ to user's exclude dictionary for _dict_. If the _word_ 474 | exists in user's personal dictionary also remove it from there." 475 | (error-if-not-active-dict dict) 476 | (error-if-not-proper-string word) 477 | (cffi:foreign-funcall "enchant_dict_remove" 478 | :pointer (address dict) 479 | :string word 480 | :int -1 481 | :void)) 482 | 483 | (defun dict-remove-from-session (dict word) 484 | "Remove _word_ from the current spell-checking session _dict_. 485 | The _word_ is not recognized anymore in the current session." 486 | (error-if-not-active-dict dict) 487 | (error-if-not-proper-string word) 488 | (cffi:foreign-funcall "enchant_dict_remove_from_session" 489 | :pointer (address dict) 490 | :string word 491 | :int -1 492 | :void)) 493 | 494 | (defun dict-is-removed-p (dict word) 495 | "Check if _word_ has been removed from user's personal dictionary or 496 | from the current spell-checking session _dict_. If true, return _word_. 497 | Otherwise return `nil`." 498 | (error-if-not-active-dict dict) 499 | (error-if-not-proper-string word) 500 | (case (cffi:foreign-funcall "enchant_dict_is_removed" 501 | :pointer (address dict) 502 | :string word 503 | :int -1 504 | :int) 505 | (1 word) 506 | (0 nil))) 507 | 508 | (defun dict-store-replacement (dict word correction) 509 | "Add a correction statement from misspelled _word_ to _correction_ 510 | using dictionary _dict_. _Correction_ might show up in the suggestion 511 | list." 512 | (error-if-not-active-dict dict) 513 | (error-if-not-proper-string word) 514 | (error-if-not-proper-string correction) 515 | (cffi:foreign-funcall "enchant_dict_store_replacement" 516 | :pointer (address dict) 517 | :string word :int -1 518 | :string correction :int -1 519 | :void)) 520 | 521 | (defun dict-describe (dict) 522 | "Describe dictionary _dict_. Return a list of four strings: language 523 | code and optional country code (e.g., `\"en\"` or `\"en_GB\"`), provider 524 | name, provider description and provider library filename. 525 | 526 | _Dict_ must be an active `dict` object returned by 527 | `broker-request-dict`, if not, signal a `not-active-dict` condition." 528 | 529 | (error-if-not-active-dict dict) 530 | (let (*callback-data*) 531 | (cffi:foreign-funcall "enchant_dict_describe" 532 | :pointer (address dict) 533 | :pointer (cffi:callback dict-describe-fn) 534 | :pointer (cffi:null-pointer) 535 | :void) 536 | (first *callback-data*))) 537 | -------------------------------------------------------------------------------- /print-doc.lisp: -------------------------------------------------------------------------------- 1 | ;; Author: Teemu Likonen 2 | ;; 3 | ;; License: Creative Commons CC0 (public domain dedication) 4 | ;; https://creativecommons.org/publicdomain/zero/1.0/legalcode 5 | 6 | (defpackage #:print-doc 7 | (:use #:cl) 8 | (:export #:print-doc)) 9 | 10 | (in-package #:print-doc) 11 | 12 | (require 'sb-introspect) 13 | 14 | (defun symbol-doc-type (symbol) 15 | (let (docs) 16 | (flet ((doc (symbol type key) 17 | (push (list symbol key (documentation symbol type)) docs))) 18 | (cond ((ignore-errors (macro-function symbol)) 19 | (doc symbol 'function :macro)) 20 | ((ignore-errors (symbol-function symbol)) 21 | (doc symbol 'function :function))) 22 | (when (ignore-errors (symbol-value symbol)) 23 | (doc symbol 'variable :variable)) 24 | (cond ((subtypep symbol 'condition) 25 | (doc symbol 'type :condition)) 26 | ((ignore-errors (find-class symbol)) 27 | (doc symbol 'type :class)))) 28 | docs)) 29 | 30 | (defun print-doc (package &key (stream *standard-output*) (prefix "### ")) 31 | (loop 32 | :with *package* := (find-package package) 33 | :with *print-right-margin* := 72 34 | :with *print-case* := :downcase 35 | :with data 36 | := (sort (loop :for symbol 37 | :being :each :external-symbol :in package 38 | :append (symbol-doc-type symbol)) 39 | (lambda (l1 l2) 40 | (let ((s1 (symbol-name (first l1))) 41 | (s2 (symbol-name (first l2))) 42 | (t1 (symbol-name (second l1))) 43 | (t2 (symbol-name (second l2)))) 44 | (or (string-lessp t1 t2) 45 | (and (string-equal t1 t2) 46 | (string-lessp s1 s2)))))) 47 | 48 | :for (symbol type doc) :in data 49 | 50 | :if doc :do 51 | 52 | (format stream "~A" prefix) 53 | (case type 54 | (:function 55 | (format stream "Function: `~A`" symbol) 56 | (let ((ll (sb-introspect:function-lambda-list symbol))) 57 | (when ll 58 | (format stream "~%~%The lambda list:~%~% ~S" ll)))) 59 | (:macro 60 | (format stream "Macro: `~A`" symbol) 61 | (let ((ll (sb-introspect:function-lambda-list symbol))) 62 | (when ll 63 | (format stream "~%~%The lambda list:~%~% ~S" ll)))) 64 | (:variable (format stream "Variable: `~A`" symbol)) 65 | (:condition (format stream "Condition: `~A`" symbol)) 66 | (:class (format stream "Class: `~A`" symbol))) 67 | (format stream "~%~%~A~%~%~%" doc))) 68 | -------------------------------------------------------------------------------- /readme.lisp: -------------------------------------------------------------------------------- 1 | (defparameter *head* 2 | "~ 3 | Enchant 4 | ======= 5 | 6 | **Common Lisp programming interface for Enchant spell-checker library** 7 | 8 | 9 | Introduction 10 | ------------ 11 | 12 | Enchant is a Common Lisp interface for the [Enchant][] spell-checker 13 | library. The Enchant library is a generic spell-checker library which 14 | uses other spell-checkers transparently as back-end. The library 15 | supports the following checkers: 16 | 17 | - Hunspell 18 | - GNU Aspell 19 | - Hspell 20 | - Voikko 21 | - Apple Spell 22 | - Zemberek 23 | 24 | This Common Lisp Enchant package uses [The Common Foreign Function 25 | Interface][CFFI] (CFFI) for accessing the Enchant C library. This 26 | package should work on any Common Lisp implementation which supports 27 | CFFI. 28 | 29 | [Enchant]: https://rrthomas.github.io/enchant/ 30 | [CFFI]: https://cffi.common-lisp.dev/ 31 | 32 | 33 | Installation 34 | ------------ 35 | 36 | [Quicklisp][QL] is the easiest way to install and load Enchant. There is 37 | one [ASDF][] system `enchant` which also loads the Enchant C library 38 | using CFFI's facilities. 39 | 40 | [QL]: https://www.quicklisp.org/ 41 | [ASDF]: https://asdf.common-lisp.dev/ 42 | 43 | 44 | Examples 45 | -------- 46 | 47 | ### Function: `(dict-check dict word)` 48 | 49 | Check the spelling for _word_ using dictionary _dict_. 50 | 51 | ENCHANT> (with-dict (lang \"en_GB\") 52 | (dict-check lang \"working\")) ; correct 53 | \"working\" 54 | 55 | ENCHANT> (with-dict (lang \"en_GB\") 56 | (dict-check lang \"wrking\")) ; incorrect 57 | NIL 58 | 59 | 60 | ### Function: `(dict-suggest dict word)` 61 | 62 | Get spelling suggestions for _word_ using dictionary _dict_. 63 | 64 | ENCHANT> (with-dict (lang \"en_US\") 65 | (dict-suggest lang \"wrking\")) 66 | (\"wring\" \"working\" \"irking\" \"waking\" \"wrying\" \"parking\" 67 | \"marking\" \"winking\" \"wicking\" \"Zworykin\" \"dragging\") 68 | 69 | 70 | License and Source Code 71 | ----------------------- 72 | 73 | Author: Teemu Likonen <> 74 | 75 | OpenPGP key: [6965F03973F0D4CA22B9410F0F2CAE0E07608462][PGP] 76 | 77 | License: [Creative Commons CC0][CC0] (public domain dedication) 78 | 79 | The source code repository: 80 | 81 | 82 | [PGP]: http://www.iki.fi/tlikonen/pgp-key.asc 83 | [CC0]: https://creativecommons.org/publicdomain/zero/1.0/legalcode 84 | 85 | 86 | The Programming Interface 87 | ------------------------- 88 | 89 | Enchant package uses similar names to the original Enchant C library. 90 | Common Lisp's naming conventions are respected, though. For example, the 91 | original C language function `enchant_dict_check()` has been named 92 | `enchant:dict-check`. The C function `enchant_broker_dict_exists()` has 93 | been named `enchant:broker-dict-exists-p` and thus respecting the Common 94 | Lisp suffix convention for predicate functions. 95 | 96 | There are also macros for convenience: `with-broker`, `with-dict` and 97 | `with-pwl-dict`. They hide some low-level resource initialization and 98 | freeing operations. 99 | 100 | 101 | ") 102 | 103 | (load "asdf.conf") 104 | (load "quicklisp/setup.lisp") 105 | 106 | (ql:quickload "enchant" :silent t) 107 | (load "print-doc.lisp") 108 | (format t *head*) 109 | (print-doc:print-doc "ENCHANT") 110 | --------------------------------------------------------------------------------