1 /* vim: set sw=8: -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * pluma-spell-checker.c
4  * This file is part of pluma
5  *
6  * Copyright (C) 2002-2006 Paolo Maggi
7  * Copyright (C) 2012-2021 MATE Developers
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin St, Fifth Floor,
22  * Boston, MA 02110-1301, USA.
23  */
24 
25 /*
26  * Modified by the pluma Team, 2002-2006. See the AUTHORS file for a
27  * list of people on the pluma Team.
28  * See the ChangeLog files for a list of changes.
29  */
30 
31 #ifdef HAVE_CONFIG_H
32 #include <config.h>
33 #endif
34 
35 #include <string.h>
36 
37 #include <enchant.h>
38 
39 #include <glib/gi18n.h>
40 #include <glib.h>
41 
42 #include "pluma-spell-checker.h"
43 #include "pluma-spell-utils.h"
44 
45 struct _PlumaSpellChecker
46 {
47 	GObject parent_instance;
48 
49 	EnchantDict                     *dict;
50 	EnchantBroker                   *broker;
51 	const PlumaSpellCheckerLanguage *active_lang;
52 };
53 
54 /* GObject properties */
55 enum {
56 	PROP_0 = 0,
57 	PROP_LANGUAGE,
58 	LAST_PROP
59 };
60 
61 /* Signals */
62 enum {
63 	ADD_WORD_TO_PERSONAL = 0,
64 	ADD_WORD_TO_SESSION,
65 	SET_LANGUAGE,
66 	CLEAR_SESSION,
67 	LAST_SIGNAL
68 };
69 
70 static guint signals[LAST_SIGNAL] = { 0 };
71 
G_DEFINE_TYPE(PlumaSpellChecker,pluma_spell_checker,G_TYPE_OBJECT)72 G_DEFINE_TYPE(PlumaSpellChecker, pluma_spell_checker, G_TYPE_OBJECT)
73 
74 static void
75 pluma_spell_checker_set_property (GObject *object,
76 			   guint prop_id,
77 			   const GValue *value,
78 			   GParamSpec *pspec)
79 {
80 	/*
81 	PlumaSpellChecker *spell = PLUMA_SPELL_CHECKER (object);
82 	*/
83 
84 	switch (prop_id)
85 	{
86 		case PROP_LANGUAGE:
87 			/* TODO */
88 			break;
89 		default:
90 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
91 	}
92 }
93 
94 static void
pluma_spell_checker_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)95 pluma_spell_checker_get_property (GObject *object,
96 			   guint prop_id,
97 			   GValue *value,
98 			   GParamSpec *pspec)
99 {
100 	/*
101 	PlumaSpellChecker *spell = PLUMA_SPELL_CHECKER (object);
102 	*/
103 
104 	switch (prop_id)
105 	{
106 		case PROP_LANGUAGE:
107 			/* TODO */
108 		default:
109 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
110 	}
111 }
112 
113 static void
pluma_spell_checker_finalize(GObject * object)114 pluma_spell_checker_finalize (GObject *object)
115 {
116 	PlumaSpellChecker *spell_checker;
117 
118 	g_return_if_fail (PLUMA_IS_SPELL_CHECKER (object));
119 
120 	spell_checker = PLUMA_SPELL_CHECKER (object);
121 
122 	if (spell_checker->dict != NULL)
123 		enchant_broker_free_dict (spell_checker->broker, spell_checker->dict);
124 
125 	if (spell_checker->broker != NULL)
126 		enchant_broker_free (spell_checker->broker);
127 
128 	G_OBJECT_CLASS (pluma_spell_checker_parent_class)->finalize (object);
129 }
130 
131 static void
pluma_spell_checker_class_init(PlumaSpellCheckerClass * klass)132 pluma_spell_checker_class_init (PlumaSpellCheckerClass * klass)
133 {
134 	GObjectClass *object_class;
135 
136 	object_class = G_OBJECT_CLASS (klass);
137 
138 	object_class->set_property = pluma_spell_checker_set_property;
139 	object_class->get_property = pluma_spell_checker_get_property;
140 
141 	object_class->finalize = pluma_spell_checker_finalize;
142 
143 	g_object_class_install_property (object_class,
144 					 PROP_LANGUAGE,
145 					 g_param_spec_pointer ("language",
146 						 	      "Language",
147 							      "The language used by the spell checker",
148 							      G_PARAM_READWRITE));
149 
150 	signals[ADD_WORD_TO_PERSONAL] =
151 	    g_signal_new ("add_word_to_personal",
152 			  G_OBJECT_CLASS_TYPE (object_class),
153 			  G_SIGNAL_RUN_LAST,
154 			  G_STRUCT_OFFSET (PlumaSpellCheckerClass, add_word_to_personal),
155 			  NULL, NULL, NULL,
156 			  G_TYPE_NONE,
157 			  2,
158 			  G_TYPE_STRING,
159 			  G_TYPE_INT);
160 
161 	signals[ADD_WORD_TO_SESSION] =
162 	    g_signal_new ("add_word_to_session",
163 			  G_OBJECT_CLASS_TYPE (object_class),
164 			  G_SIGNAL_RUN_LAST,
165 			  G_STRUCT_OFFSET (PlumaSpellCheckerClass, add_word_to_session),
166 			  NULL, NULL, NULL,
167 			  G_TYPE_NONE,
168 			  2,
169 			  G_TYPE_STRING,
170 			  G_TYPE_INT);
171 
172 	signals[SET_LANGUAGE] =
173 	    g_signal_new ("set_language",
174 			  G_OBJECT_CLASS_TYPE (object_class),
175 			  G_SIGNAL_RUN_LAST,
176 			  G_STRUCT_OFFSET (PlumaSpellCheckerClass, set_language),
177 			  NULL, NULL, NULL,
178 			  G_TYPE_NONE,
179 			  1,
180 			  G_TYPE_POINTER);
181 
182 	signals[CLEAR_SESSION] =
183 	    g_signal_new ("clear_session",
184 			  G_OBJECT_CLASS_TYPE (object_class),
185 			  G_SIGNAL_RUN_LAST,
186 			  G_STRUCT_OFFSET (PlumaSpellCheckerClass, clear_session),
187 			  NULL, NULL, NULL,
188 			  G_TYPE_NONE,
189 			  0);
190 }
191 
192 static void
pluma_spell_checker_init(PlumaSpellChecker * spell_checker)193 pluma_spell_checker_init (PlumaSpellChecker *spell_checker)
194 {
195 	spell_checker->broker = enchant_broker_init ();
196 	spell_checker->dict = NULL;
197 	spell_checker->active_lang = NULL;
198 }
199 
200 PlumaSpellChecker *
pluma_spell_checker_new(void)201 pluma_spell_checker_new	(void)
202 {
203 	PlumaSpellChecker *spell;
204 
205 	spell = PLUMA_SPELL_CHECKER (
206 			g_object_new (PLUMA_TYPE_SPELL_CHECKER, NULL));
207 
208 	g_return_val_if_fail (spell != NULL, NULL);
209 
210 	return spell;
211 }
212 
213 static gboolean
lazy_init(PlumaSpellChecker * spell,const PlumaSpellCheckerLanguage * language)214 lazy_init (PlumaSpellChecker               *spell,
215 	   const PlumaSpellCheckerLanguage *language)
216 {
217 	if (spell->dict != NULL)
218 		return TRUE;
219 
220 	g_return_val_if_fail (spell->broker != NULL, FALSE);
221 
222 	spell->active_lang = NULL;
223 
224 	if (language != NULL)
225 	{
226 		spell->active_lang = language;
227 	}
228 	else
229 	{
230 		/* First try to get a default language */
231 		const PlumaSpellCheckerLanguage *l;
232 		gint i = 0;
233 		const gchar * const *lang_tags = g_get_language_names ();
234 
235 		while (lang_tags [i])
236 		{
237 			l = pluma_spell_checker_language_from_key (lang_tags [i]);
238 
239 			if (l != NULL)
240 			{
241 				spell->active_lang = l;
242 				break;
243 			}
244 
245 			i++;
246 		}
247 	}
248 
249 	/* Second try to get a default language */
250 	if (spell->active_lang == NULL)
251 	{
252 		spell->active_lang = pluma_spell_checker_language_from_key ("en_US");
253 
254 		/* Last try to get a default language */
255 		if (spell->active_lang == NULL)
256 		{
257 			const GSList *langs;
258 			langs = pluma_spell_checker_get_available_languages ();
259 			if (langs != NULL)
260 				spell->active_lang = (const PlumaSpellCheckerLanguage *)langs->data;
261 		}
262 	}
263 
264 	if (spell->active_lang != NULL)
265 	{
266 		const gchar *key;
267 
268 		key = pluma_spell_checker_language_to_key (spell->active_lang);
269 
270 		spell->dict = enchant_broker_request_dict (spell->broker,
271 							   key);
272 	}
273 
274 	if (spell->dict == NULL)
275 	{
276 		spell->active_lang = NULL;
277 
278 		if (language != NULL)
279 			g_warning ("Spell checker plugin: cannot select a default language.");
280 
281 		return FALSE;
282 	}
283 
284 	return TRUE;
285 }
286 
287 gboolean
pluma_spell_checker_set_language(PlumaSpellChecker * spell,const PlumaSpellCheckerLanguage * language)288 pluma_spell_checker_set_language (PlumaSpellChecker               *spell,
289 				  const PlumaSpellCheckerLanguage *language)
290 {
291 	gboolean ret;
292 
293 	g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), FALSE);
294 
295 	if (spell->dict != NULL)
296 	{
297 		enchant_broker_free_dict (spell->broker, spell->dict);
298 		spell->dict = NULL;
299 	}
300 
301 	ret = lazy_init (spell, language);
302 
303 	if (ret)
304 		g_signal_emit (G_OBJECT (spell), signals[SET_LANGUAGE], 0, language);
305 	else
306 		g_warning ("Spell checker plugin: cannot use language %s.",
307 		           pluma_spell_checker_language_to_string (language));
308 
309 	return ret;
310 }
311 
312 const PlumaSpellCheckerLanguage *
pluma_spell_checker_get_language(PlumaSpellChecker * spell)313 pluma_spell_checker_get_language (PlumaSpellChecker *spell)
314 {
315 	g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), NULL);
316 
317 	if (!lazy_init (spell, spell->active_lang))
318 		return NULL;
319 
320 	return spell->active_lang;
321 }
322 
323 gboolean
pluma_spell_checker_check_word(PlumaSpellChecker * spell,const gchar * word,gssize len)324 pluma_spell_checker_check_word (PlumaSpellChecker *spell,
325 				const gchar       *word,
326 				gssize             len)
327 {
328 	gint enchant_result;
329 	gboolean res = FALSE;
330 
331 	g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), FALSE);
332 	g_return_val_if_fail (word != NULL, FALSE);
333 
334 	if (!lazy_init (spell, spell->active_lang))
335 		return FALSE;
336 
337 	if (len < 0)
338 		len = strlen (word);
339 
340 	if (strcmp (word, "pluma") == 0)
341 		return TRUE;
342 
343 	if (pluma_spell_utils_is_digit (word, len))
344 		return TRUE;
345 
346 	g_return_val_if_fail (spell->dict != NULL, FALSE);
347 	enchant_result = enchant_dict_check (spell->dict, word, len);
348 
349 	switch (enchant_result)
350 	{
351 		case -1:
352 			/* error */
353 			res = FALSE;
354 
355 			g_warning ("Spell checker plugin: error checking word '%s' (%s).",
356 				   word, enchant_dict_get_error (spell->dict));
357 
358 			break;
359 		case 1:
360 			/* it is not in the directory */
361 			res = FALSE;
362 			break;
363 		case 0:
364 			/* is is in the directory */
365 			res = TRUE;
366 			break;
367 		default:
368 			g_return_val_if_reached (FALSE);
369 	}
370 
371 	return res;
372 }
373 
374 
375 /* return NULL on error or if no suggestions are found */
376 GSList *
pluma_spell_checker_get_suggestions(PlumaSpellChecker * spell,const gchar * word,gssize len)377 pluma_spell_checker_get_suggestions (PlumaSpellChecker *spell,
378 				     const gchar       *word,
379 				     gssize             len)
380 {
381 	gchar **suggestions;
382 	size_t n_suggestions = 0;
383 	GSList *suggestions_list = NULL;
384 	gint i;
385 
386 	g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), NULL);
387 	g_return_val_if_fail (word != NULL, NULL);
388 
389 	if (!lazy_init (spell, spell->active_lang))
390 		return NULL;
391 
392 	g_return_val_if_fail (spell->dict != NULL, NULL);
393 
394 	if (len < 0)
395 		len = strlen (word);
396 
397 	suggestions = enchant_dict_suggest (spell->dict, word, len, &n_suggestions);
398 
399 	if (n_suggestions == 0)
400 		return NULL;
401 
402 	g_return_val_if_fail (suggestions != NULL, NULL);
403 
404 	for (i = 0; i < (gint)n_suggestions; i++)
405 	{
406 		suggestions_list = g_slist_prepend (suggestions_list,
407 						    suggestions[i]);
408 	}
409 
410 	/* The single suggestions will be freed by the caller */
411 	g_free (suggestions);
412 
413 	suggestions_list = g_slist_reverse (suggestions_list);
414 
415 	return suggestions_list;
416 }
417 
418 gboolean
pluma_spell_checker_add_word_to_personal(PlumaSpellChecker * spell,const gchar * word,gssize len)419 pluma_spell_checker_add_word_to_personal (PlumaSpellChecker *spell,
420 					  const gchar       *word,
421 					  gssize             len)
422 {
423 	g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), FALSE);
424 	g_return_val_if_fail (word != NULL, FALSE);
425 
426 	if (!lazy_init (spell, spell->active_lang))
427 		return FALSE;
428 
429 	g_return_val_if_fail (spell->dict != NULL, FALSE);
430 
431 	if (len < 0)
432 		len = strlen (word);
433 
434 	enchant_dict_add (spell->dict, word, len);
435 
436 
437 	g_signal_emit (G_OBJECT (spell), signals[ADD_WORD_TO_PERSONAL], 0, word, len);
438 
439 	return TRUE;
440 }
441 
442 gboolean
pluma_spell_checker_add_word_to_session(PlumaSpellChecker * spell,const gchar * word,gssize len)443 pluma_spell_checker_add_word_to_session (PlumaSpellChecker *spell,
444 					 const gchar       *word,
445 					 gssize             len)
446 {
447 	g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), FALSE);
448 	g_return_val_if_fail (word != NULL, FALSE);
449 
450 	if (!lazy_init (spell, spell->active_lang))
451 		return FALSE;
452 
453 	g_return_val_if_fail (spell->dict != NULL, FALSE);
454 
455 	if (len < 0)
456 		len = strlen (word);
457 
458 	enchant_dict_add_to_session (spell->dict, word, len);
459 
460 	g_signal_emit (G_OBJECT (spell), signals[ADD_WORD_TO_SESSION], 0, word, len);
461 
462 	return TRUE;
463 }
464 
465 gboolean
pluma_spell_checker_clear_session(PlumaSpellChecker * spell)466 pluma_spell_checker_clear_session (PlumaSpellChecker *spell)
467 {
468 	g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), FALSE);
469 
470 	/* free and re-request dictionary */
471 	if (spell->dict != NULL)
472 	{
473 		enchant_broker_free_dict (spell->broker, spell->dict);
474 		spell->dict = NULL;
475 	}
476 
477 	if (!lazy_init (spell, spell->active_lang))
478 		return FALSE;
479 
480 	g_signal_emit (G_OBJECT (spell), signals[CLEAR_SESSION], 0);
481 
482 	return TRUE;
483 }
484 
485 /*
486  * Informs dictionary, that word 'word' will be replaced/corrected by word
487  * 'replacement'
488  */
489 gboolean
pluma_spell_checker_set_correction(PlumaSpellChecker * spell,const gchar * word,gssize w_len,const gchar * replacement,gssize r_len)490 pluma_spell_checker_set_correction (PlumaSpellChecker *spell,
491 				    const gchar       *word,
492 				    gssize             w_len,
493 				    const gchar       *replacement,
494 				    gssize             r_len)
495 {
496 	g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (spell), FALSE);
497 	g_return_val_if_fail (word != NULL, FALSE);
498 	g_return_val_if_fail (replacement != NULL, FALSE);
499 
500 	if (!lazy_init (spell, spell->active_lang))
501 		return FALSE;
502 
503 	g_return_val_if_fail (spell->dict != NULL, FALSE);
504 
505 	if (w_len < 0)
506 		w_len = strlen (word);
507 
508 	if (r_len < 0)
509 		r_len = strlen (replacement);
510 
511 	enchant_dict_store_replacement (spell->dict,
512 					word,
513 					w_len,
514 					replacement,
515 					r_len);
516 
517 	return TRUE;
518 }
519 
520