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