1 /*
2 * This file is part of gspell, a spell-checking library.
3 *
4 * Copyright 2002-2006 - Paolo Maggi
5 * Copyright 2015, 2016 - Sébastien Wilmet
6 *
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public
9 * License as published by the Free Software Foundation; either
10 * version 2.1 of the License, or (at your option) any later version.
11 *
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public License
18 * along with this library; if not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #ifdef HAVE_CONFIG_H
22 #include "config.h"
23 #endif
24
25 #include "gspell-checker.h"
26 #include "gspell-checker-private.h"
27 #include <glib/gi18n-lib.h>
28 #include <string.h>
29 #include "gspell-utils.h"
30
31 #ifdef OS_OSX
32 #include "gspell-osx.h"
33 #endif
34
35 /**
36 * SECTION:checker
37 * @Short_description: Spell checker
38 * @Title: GspellChecker
39 * @See_also: #GspellLanguage
40 *
41 * #GspellChecker is a spell checker.
42 *
43 * If the #GspellChecker:language property is %NULL, it means that no
44 * dictonaries are available, in which case the #GspellChecker is in a
45 * “disabled” (but allowed) state.
46 *
47 * gspell uses the [Enchant](https://abiword.github.io/enchant/) library. The
48 * use of Enchant is part of the gspell API, #GspellChecker exposes the
49 * EnchantDict with the gspell_checker_get_enchant_dict() function.
50 */
51
52 typedef struct _GspellCheckerPrivate GspellCheckerPrivate;
53
54 struct _GspellCheckerPrivate
55 {
56 EnchantBroker *broker;
57 EnchantDict *dict;
58 const GspellLanguage *active_lang;
59 };
60
61 enum
62 {
63 PROP_0,
64 PROP_LANGUAGE,
65 };
66
67 enum
68 {
69 SIGNAL_WORD_ADDED_TO_PERSONAL,
70 SIGNAL_WORD_ADDED_TO_SESSION,
71 SIGNAL_SESSION_CLEARED,
72 LAST_SIGNAL
73 };
74
75 static guint signals[LAST_SIGNAL] = { 0 };
76
G_DEFINE_TYPE_WITH_PRIVATE(GspellChecker,gspell_checker,G_TYPE_OBJECT)77 G_DEFINE_TYPE_WITH_PRIVATE (GspellChecker, gspell_checker, G_TYPE_OBJECT)
78
79 GQuark
80 gspell_checker_error_quark (void)
81 {
82 static GQuark quark = 0;
83
84 if (G_UNLIKELY (quark == 0))
85 {
86 quark = g_quark_from_static_string ("gspell-checker-error-quark");
87 }
88
89 return quark;
90 }
91
92 static void
gspell_checker_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)93 gspell_checker_set_property (GObject *object,
94 guint prop_id,
95 const GValue *value,
96 GParamSpec *pspec)
97 {
98 GspellChecker *checker = GSPELL_CHECKER (object);
99
100 switch (prop_id)
101 {
102 case PROP_LANGUAGE:
103 gspell_checker_set_language (checker, g_value_get_boxed (value));
104 break;
105
106 default:
107 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
108 break;
109 }
110 }
111
112 static void
gspell_checker_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)113 gspell_checker_get_property (GObject *object,
114 guint prop_id,
115 GValue *value,
116 GParamSpec *pspec)
117 {
118 GspellChecker *checker = GSPELL_CHECKER (object);
119
120 switch (prop_id)
121 {
122 case PROP_LANGUAGE:
123 g_value_set_boxed (value, gspell_checker_get_language (checker));
124 break;
125
126 default:
127 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
128 break;
129 }
130 }
131
132 static void
gspell_checker_finalize(GObject * object)133 gspell_checker_finalize (GObject *object)
134 {
135 GspellCheckerPrivate *priv;
136
137 priv = gspell_checker_get_instance_private (GSPELL_CHECKER (object));
138
139 if (priv->dict != NULL)
140 {
141 enchant_broker_free_dict (priv->broker, priv->dict);
142 }
143
144 if (priv->broker != NULL)
145 {
146 enchant_broker_free (priv->broker);
147 }
148
149 G_OBJECT_CLASS (gspell_checker_parent_class)->finalize (object);
150 }
151
152 static void
gspell_checker_class_init(GspellCheckerClass * klass)153 gspell_checker_class_init (GspellCheckerClass *klass)
154 {
155 GObjectClass *object_class = G_OBJECT_CLASS (klass);
156
157 object_class->set_property = gspell_checker_set_property;
158 object_class->get_property = gspell_checker_get_property;
159 object_class->finalize = gspell_checker_finalize;
160
161 /**
162 * GspellChecker:language:
163 *
164 * The #GspellLanguage used.
165 */
166 g_object_class_install_property (object_class,
167 PROP_LANGUAGE,
168 g_param_spec_boxed ("language",
169 "Language",
170 "",
171 GSPELL_TYPE_LANGUAGE,
172 G_PARAM_READWRITE |
173 G_PARAM_CONSTRUCT |
174 G_PARAM_STATIC_STRINGS));
175
176 /**
177 * GspellChecker::word-added-to-personal:
178 * @spell_checker: the #GspellChecker.
179 * @word: the added word.
180 *
181 * Emitted when a word is added to the personal dictionary.
182 */
183 signals[SIGNAL_WORD_ADDED_TO_PERSONAL] =
184 g_signal_new ("word-added-to-personal",
185 G_OBJECT_CLASS_TYPE (object_class),
186 G_SIGNAL_RUN_LAST,
187 G_STRUCT_OFFSET (GspellCheckerClass, word_added_to_personal),
188 NULL, NULL, NULL,
189 G_TYPE_NONE,
190 1,
191 G_TYPE_STRING);
192
193 /**
194 * GspellChecker::word-added-to-session:
195 * @spell_checker: the #GspellChecker.
196 * @word: the added word.
197 *
198 * Emitted when a word is added to the session dictionary. See
199 * gspell_checker_add_word_to_session().
200 */
201 signals[SIGNAL_WORD_ADDED_TO_SESSION] =
202 g_signal_new ("word-added-to-session",
203 G_OBJECT_CLASS_TYPE (object_class),
204 G_SIGNAL_RUN_LAST,
205 G_STRUCT_OFFSET (GspellCheckerClass, word_added_to_session),
206 NULL, NULL, NULL,
207 G_TYPE_NONE,
208 1,
209 G_TYPE_STRING);
210
211 /**
212 * GspellChecker::session-cleared:
213 * @spell_checker: the #GspellChecker.
214 *
215 * Emitted when the session dictionary is cleared.
216 */
217 signals[SIGNAL_SESSION_CLEARED] =
218 g_signal_new ("session-cleared",
219 G_OBJECT_CLASS_TYPE (object_class),
220 G_SIGNAL_RUN_LAST,
221 G_STRUCT_OFFSET (GspellCheckerClass, session_cleared),
222 NULL, NULL, NULL,
223 G_TYPE_NONE,
224 0);
225 }
226
227 static void
gspell_checker_init(GspellChecker * checker)228 gspell_checker_init (GspellChecker *checker)
229 {
230 GspellCheckerPrivate *priv;
231
232 priv = gspell_checker_get_instance_private (checker);
233
234 priv->broker = enchant_broker_init ();
235 priv->dict = NULL;
236 priv->active_lang = NULL;
237 }
238
239 /**
240 * gspell_checker_new:
241 * @language: (nullable): the #GspellLanguage to use, or %NULL.
242 *
243 * Creates a new #GspellChecker. If @language is %NULL, the default language is
244 * picked with gspell_language_get_default().
245 *
246 * Returns: a new #GspellChecker object.
247 */
248 GspellChecker *
gspell_checker_new(const GspellLanguage * language)249 gspell_checker_new (const GspellLanguage *language)
250 {
251 return g_object_new (GSPELL_TYPE_CHECKER,
252 "language", language,
253 NULL);
254 }
255
256 static void
create_new_dictionary(GspellChecker * checker)257 create_new_dictionary (GspellChecker *checker)
258 {
259 GspellCheckerPrivate *priv;
260 const gchar *language_code;
261 const gchar *app_name;
262
263 priv = gspell_checker_get_instance_private (checker);
264
265 if (priv->dict != NULL)
266 {
267 enchant_broker_free_dict (priv->broker, priv->dict);
268 priv->dict = NULL;
269 }
270
271 if (priv->active_lang == NULL)
272 {
273 return;
274 }
275
276 language_code = gspell_language_get_code (priv->active_lang);
277 priv->dict = enchant_broker_request_dict (priv->broker, language_code);
278
279 if (priv->dict == NULL)
280 {
281 /* Should never happen, no need to return a GError. */
282 g_warning ("Impossible to create an Enchant dictionary for the language code '%s'.",
283 language_code);
284
285 priv->active_lang = NULL;
286 return;
287 }
288
289 app_name = g_get_application_name ();
290 gspell_checker_add_word_to_session (checker, app_name, -1);
291 }
292
293 /* Used for unit tests. Useful to force a NULL language. */
294 void
_gspell_checker_force_set_language(GspellChecker * checker,const GspellLanguage * language)295 _gspell_checker_force_set_language (GspellChecker *checker,
296 const GspellLanguage *language)
297 {
298 GspellCheckerPrivate *priv;
299
300 g_return_if_fail (GSPELL_IS_CHECKER (checker));
301
302 priv = gspell_checker_get_instance_private (checker);
303
304 if (priv->active_lang != language)
305 {
306 priv->active_lang = language;
307 create_new_dictionary (checker);
308 g_object_notify (G_OBJECT (checker), "language");
309 }
310 }
311
312 /**
313 * gspell_checker_set_language:
314 * @checker: a #GspellChecker.
315 * @language: (nullable): the #GspellLanguage to use, or %NULL.
316 *
317 * Sets the language to use for the spell checking. If @language is %NULL, the
318 * default language is picked with gspell_language_get_default().
319 */
320 void
gspell_checker_set_language(GspellChecker * checker,const GspellLanguage * language)321 gspell_checker_set_language (GspellChecker *checker,
322 const GspellLanguage *language)
323 {
324 g_return_if_fail (GSPELL_IS_CHECKER (checker));
325
326 if (language == NULL)
327 {
328 language = gspell_language_get_default ();
329 }
330
331 _gspell_checker_force_set_language (checker, language);
332 }
333
334 /**
335 * gspell_checker_get_language:
336 * @checker: a #GspellChecker.
337 *
338 * Returns: (nullable): the #GspellLanguage currently used, or %NULL
339 * if no dictionaries are available.
340 */
341 const GspellLanguage *
gspell_checker_get_language(GspellChecker * checker)342 gspell_checker_get_language (GspellChecker *checker)
343 {
344 GspellCheckerPrivate *priv;
345
346 g_return_val_if_fail (GSPELL_IS_CHECKER (checker), NULL);
347
348 priv = gspell_checker_get_instance_private (checker);
349
350 return priv->active_lang;
351 }
352
353 /**
354 * gspell_checker_check_word:
355 * @checker: a #GspellChecker.
356 * @word: the word to check.
357 * @word_length: the byte length of @word, or -1 if @word is nul-terminated.
358 * @error: (out) (optional): a location to a %NULL #GError, or %NULL.
359 *
360 * If the #GspellChecker:language is %NULL, i.e. when no dictonaries are
361 * available, this function returns %TRUE to limit the damage.
362 *
363 * Returns: %TRUE if @word is correctly spelled, %FALSE otherwise.
364 */
365 gboolean
gspell_checker_check_word(GspellChecker * checker,const gchar * word,gssize word_length,GError ** error)366 gspell_checker_check_word (GspellChecker *checker,
367 const gchar *word,
368 gssize word_length,
369 GError **error)
370 {
371 GspellCheckerPrivate *priv;
372 gint enchant_result;
373 gboolean correctly_spelled;
374 gchar *sanitized_word;
375
376 g_return_val_if_fail (GSPELL_IS_CHECKER (checker), FALSE);
377 g_return_val_if_fail (word != NULL, FALSE);
378 g_return_val_if_fail (word_length >= -1, FALSE);
379 g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
380
381 priv = gspell_checker_get_instance_private (checker);
382
383 if (priv->dict == NULL)
384 {
385 return TRUE;
386 }
387
388 if (_gspell_utils_is_number (word, word_length))
389 {
390 return TRUE;
391 }
392
393 if (_gspell_utils_str_to_ascii_apostrophe (word, word_length, &sanitized_word))
394 {
395 enchant_result = enchant_dict_check (priv->dict, sanitized_word, -1);
396 g_free (sanitized_word);
397 }
398 else
399 {
400 enchant_result = enchant_dict_check (priv->dict, word, word_length);
401 }
402
403 correctly_spelled = enchant_result == 0;
404
405 if (enchant_result < 0)
406 {
407 gchar *nul_terminated_word;
408
409 if (word_length == -1)
410 {
411 word_length = strlen (word);
412 }
413
414 nul_terminated_word = g_strndup (word, word_length);
415
416 g_set_error (error,
417 GSPELL_CHECKER_ERROR,
418 GSPELL_CHECKER_ERROR_DICTIONARY,
419 _("Error when checking the spelling of word “%s”: %s"),
420 nul_terminated_word,
421 enchant_dict_get_error (priv->dict));
422
423 g_free (nul_terminated_word);
424 }
425
426 return correctly_spelled;
427 }
428
429 /**
430 * gspell_checker_get_suggestions:
431 * @checker: a #GspellChecker.
432 * @word: a misspelled word.
433 * @word_length: the byte length of @word, or -1 if @word is nul-terminated.
434 *
435 * Gets the suggestions for @word. Free the return value with
436 * g_slist_free_full(suggestions, g_free).
437 *
438 * Returns: (transfer full) (element-type utf8): the list of suggestions.
439 */
440 GSList *
gspell_checker_get_suggestions(GspellChecker * checker,const gchar * word,gssize word_length)441 gspell_checker_get_suggestions (GspellChecker *checker,
442 const gchar *word,
443 gssize word_length)
444 {
445 GspellCheckerPrivate *priv;
446 gchar *sanitized_word;
447 gchar **suggestions;
448 GSList *suggestions_list = NULL;
449 gint i;
450
451 g_return_val_if_fail (GSPELL_IS_CHECKER (checker), NULL);
452 g_return_val_if_fail (word != NULL, NULL);
453 g_return_val_if_fail (word_length >= -1, NULL);
454
455 priv = gspell_checker_get_instance_private (checker);
456
457 if (priv->dict == NULL)
458 {
459 return NULL;
460 }
461
462 if (_gspell_utils_str_to_ascii_apostrophe (word, word_length, &sanitized_word))
463 {
464 suggestions = enchant_dict_suggest (priv->dict, sanitized_word, -1, NULL);
465 g_free (sanitized_word);
466 }
467 else
468 {
469 suggestions = enchant_dict_suggest (priv->dict, word, word_length, NULL);
470 }
471
472 if (suggestions == NULL)
473 {
474 return NULL;
475 }
476
477 for (i = 0; suggestions[i] != NULL; i++)
478 {
479 suggestions_list = g_slist_prepend (suggestions_list, suggestions[i]);
480 }
481
482 /* The array/list elements will be freed by the caller. */
483 g_free (suggestions);
484
485 return g_slist_reverse (suggestions_list);
486 }
487
488 /**
489 * gspell_checker_add_word_to_personal:
490 * @checker: a #GspellChecker.
491 * @word: a word.
492 * @word_length: the byte length of @word, or -1 if @word is nul-terminated.
493 *
494 * Adds a word to the personal dictionary. It is typically saved in the user's
495 * home directory.
496 */
497 void
gspell_checker_add_word_to_personal(GspellChecker * checker,const gchar * word,gssize word_length)498 gspell_checker_add_word_to_personal (GspellChecker *checker,
499 const gchar *word,
500 gssize word_length)
501 {
502 GspellCheckerPrivate *priv;
503
504 g_return_if_fail (GSPELL_IS_CHECKER (checker));
505 g_return_if_fail (word != NULL);
506 g_return_if_fail (word_length >= -1);
507
508 priv = gspell_checker_get_instance_private (checker);
509
510 if (priv->dict == NULL)
511 {
512 return;
513 }
514
515 enchant_dict_add (priv->dict, word, word_length);
516
517 if (word_length == -1)
518 {
519 g_signal_emit (G_OBJECT (checker),
520 signals[SIGNAL_WORD_ADDED_TO_PERSONAL], 0,
521 word);
522 }
523 else
524 {
525 gchar *nul_terminated_word = g_strndup (word, word_length);
526
527 g_signal_emit (G_OBJECT (checker),
528 signals[SIGNAL_WORD_ADDED_TO_PERSONAL], 0,
529 nul_terminated_word);
530
531 g_free (nul_terminated_word);
532 }
533 }
534
535 /**
536 * gspell_checker_add_word_to_session:
537 * @checker: a #GspellChecker.
538 * @word: a word.
539 * @word_length: the byte length of @word, or -1 if @word is nul-terminated.
540 *
541 * Adds a word to the session dictionary. Each #GspellChecker instance has a
542 * different session dictionary. The session dictionary is lost when the
543 * #GspellChecker:language property changes or when @checker is destroyed or
544 * when gspell_checker_clear_session() is called.
545 *
546 * This function is typically called for an “Ignore All” action.
547 */
548 void
gspell_checker_add_word_to_session(GspellChecker * checker,const gchar * word,gssize word_length)549 gspell_checker_add_word_to_session (GspellChecker *checker,
550 const gchar *word,
551 gssize word_length)
552 {
553 GspellCheckerPrivate *priv;
554
555 g_return_if_fail (GSPELL_IS_CHECKER (checker));
556 g_return_if_fail (word != NULL);
557 g_return_if_fail (word_length >= -1);
558
559 priv = gspell_checker_get_instance_private (checker);
560
561 if (priv->dict == NULL)
562 {
563 return;
564 }
565
566 enchant_dict_add_to_session (priv->dict, word, word_length);
567
568 if (word_length == -1)
569 {
570 g_signal_emit (G_OBJECT (checker),
571 signals[SIGNAL_WORD_ADDED_TO_SESSION], 0,
572 word);
573 }
574 else
575 {
576 gchar *nul_terminated_word = g_strndup (word, word_length);
577
578 g_signal_emit (G_OBJECT (checker),
579 signals[SIGNAL_WORD_ADDED_TO_SESSION], 0,
580 nul_terminated_word);
581
582 g_free (nul_terminated_word);
583 }
584 }
585
586 /**
587 * gspell_checker_clear_session:
588 * @checker: a #GspellChecker.
589 *
590 * Clears the session dictionary.
591 */
592 void
gspell_checker_clear_session(GspellChecker * checker)593 gspell_checker_clear_session (GspellChecker *checker)
594 {
595 g_return_if_fail (GSPELL_IS_CHECKER (checker));
596
597 /* Free and re-request dictionary. */
598 create_new_dictionary (checker);
599
600 g_signal_emit (G_OBJECT (checker), signals[SIGNAL_SESSION_CLEARED], 0);
601 }
602
603 /**
604 * gspell_checker_set_correction:
605 * @checker: a #GspellChecker.
606 * @word: a word.
607 * @word_length: the byte length of @word, or -1 if @word is nul-terminated.
608 * @replacement: the replacement word.
609 * @replacement_length: the byte length of @replacement, or -1 if @replacement
610 * is nul-terminated.
611 *
612 * Informs the spell checker that @word is replaced/corrected by @replacement.
613 */
614 void
gspell_checker_set_correction(GspellChecker * checker,const gchar * word,gssize word_length,const gchar * replacement,gssize replacement_length)615 gspell_checker_set_correction (GspellChecker *checker,
616 const gchar *word,
617 gssize word_length,
618 const gchar *replacement,
619 gssize replacement_length)
620 {
621 GspellCheckerPrivate *priv;
622
623 g_return_if_fail (GSPELL_IS_CHECKER (checker));
624 g_return_if_fail (word != NULL);
625 g_return_if_fail (word_length >= -1);
626 g_return_if_fail (replacement != NULL);
627 g_return_if_fail (replacement_length >= -1);
628
629 priv = gspell_checker_get_instance_private (checker);
630
631 if (priv->dict == NULL)
632 {
633 return;
634 }
635
636 enchant_dict_store_replacement (priv->dict,
637 word, word_length,
638 replacement, replacement_length);
639 }
640
641 /**
642 * gspell_checker_get_enchant_dict: (skip)
643 * @checker: a #GspellChecker.
644 *
645 * Gets the EnchantDict currently used by @checker. It permits to extend
646 * #GspellChecker with more features. Note that by doing so, the other classes
647 * in gspell may no longer work well.
648 *
649 * #GspellChecker re-creates a new EnchantDict when the #GspellChecker:language
650 * is changed and when the session is cleared.
651 *
652 * Returns: (transfer none) (nullable): the EnchantDict currently used by
653 * @checker.
654 * Since: 1.6
655 */
656 EnchantDict *
gspell_checker_get_enchant_dict(GspellChecker * checker)657 gspell_checker_get_enchant_dict (GspellChecker *checker)
658 {
659 GspellCheckerPrivate *priv;
660
661 g_return_val_if_fail (GSPELL_IS_CHECKER (checker), NULL);
662
663 priv = gspell_checker_get_instance_private (checker);
664 return priv->dict;
665 }
666
667 /* ex:set ts=8 noet: */
668