* Copyright 2011 kubtek <kubtek@mail.com>
*
* This file is part of StarDict.
*
* StarDict is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* StarDict is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with StarDict. If not, see <http://www.gnu.org/licenses/>.
*/
#include "stardict_spell.h"
#include <glib.h>
#include <glib/gi18n.h>
#include <enchant.h>
#include <pango/pango.h>
#include <cstring>
#include <string>
#include <vector>
static const StarDictPluginSystemInfo *plugin_info = NULL;
static const StarDictPluginSystemService *plugin_service;
static IAppDirs* gpAppDirs = NULL;
static EnchantBroker *broker = NULL;
static std::list<EnchantDict *> dictlist;
static PangoLayout *layout = NULL;
static gboolean use_custom;
static std::string custom_langs;
static std::string build_path(const std::string& path1, const std::string& path2)
{
std::string res;
res.reserve(path1.length() + 1 + path2.length());
res = path1;
if(!res.empty() && res[res.length()-1] != G_DIR_SEPARATOR)
res += G_DIR_SEPARATOR_S;
if(!path2.empty() && path2[0] == G_DIR_SEPARATOR)
res.append(path2, 1, std::string::npos);
else
res.append(path2);
return res;
}
static std::string get_cfg_filename()
{
return build_path(gpAppDirs->get_user_config_dir(), "spell.cfg");
}
static void stardict_strsplit_utf8(const gchar *text, gchar ***set, gint **starts, gint **ends)
{
PangoLogAttr *log_attrs;
gint n_attrs, n_strings, i, j;
pango_layout_get_log_attrs(layout, &log_attrs, &n_attrs);
n_strings = 0;
for (i = 0; i < n_attrs; i++)
if (log_attrs[i].is_word_start)
n_strings++;
*set = g_new0(gchar *, n_strings + 1);
*starts = g_new0(gint, n_strings);
*ends = g_new0(gint, n_strings);
for (i = 0, j = 0; i < n_attrs; i++) {
if (log_attrs[i].is_word_start) {
gint cend, bytes;
gchar *start;
cend = i;
while (!(log_attrs[cend].is_word_end))
cend++;
start = g_utf8_offset_to_pointer(text, i);
bytes = (gint) (g_utf8_offset_to_pointer(text, cend) - start);
(*set)[j] = g_new0(gchar, bytes + 1);
(*starts)[j] = (gint) (start - text);
(*ends)[j] = (gint) (start - text + bytes);
g_utf8_strncpy((*set)[j], start, cend - i);
j++;
}
}
g_free (log_attrs);
}
static void lookup(const char *text, char ***pppWord, char ****ppppWordData)
{
size_t len = strlen(text);
pango_layout_set_text(layout, text, len);
gchar **words;
gint *word_starts;
gint *word_ends;
stardict_strsplit_utf8(text, &words, &word_starts, &word_ends);
std::list<std::string> misspelled_wordlist;
gchar *spellword;
int start, end;
bool misspelled;
int *insert_tags = (int *)g_malloc0(sizeof(int)*(len +1));
int n_words;
for (n_words = 0; words[n_words]; n_words++) {
if (words[n_words][0] == '\0')
continue;
start = word_starts[n_words];
end = word_ends[n_words];
if (start == end)
continue;
spellword = g_new0(gchar, end - start + 2);
g_strlcpy(spellword, text + start, end - start + 1);
if (g_unichar_isalpha(*spellword) == FALSE) {
misspelled = false;
} else {
misspelled = true;
size_t wordlen = strlen(spellword);
for (std::list<EnchantDict *>::iterator iter = dictlist.begin(); iter != dictlist.end(); ++iter) {
if (enchant_dict_check(*iter, spellword, wordlen) <= 0) {
misspelled = false;
break;
}
}
}
if (misspelled) {
misspelled_wordlist.push_back(spellword);
insert_tags[start] = 1;
insert_tags[end] = 2;
}
g_free(spellword);
}
std::string underline_str;
int n_misspelled = misspelled_wordlist.size();
if (n_misspelled !=0 && n_words != 1) {
underline_str += "<big>";
for (size_t i = 0; i < len; i++) {
if (insert_tags[i] == 1) {
underline_str += "<span underline=\"error\" underline_color=\"#FF0000\">";
} else if (insert_tags[i] == 2) {
underline_str += "</span>";
}
switch (text[i]) {
case '&':
underline_str += "&";
break;
case '<':
underline_str += "<";
break;
case '>':
underline_str += ">";
break;
case '\'':
underline_str += "'";
break;
case '"':
underline_str += """;
break;
default:
underline_str += text[i];
}
}
if (insert_tags[len] == 2)
underline_str += "</span>";
underline_str += "</big>";
}
g_free(insert_tags);
g_strfreev(words);
g_free(word_starts);
g_free(word_ends);
std::vector< std::pair<char *, char *> > result;
char **suggestion;
for (std::list<std::string>::iterator iter = misspelled_wordlist.begin(); iter != misspelled_wordlist.end(); ++iter) {
std::list<char **> suggestions;
for (std::list<EnchantDict *>::iterator i = dictlist.begin(); i != dictlist.end(); ++i) {
suggestion = enchant_dict_suggest(*i, (*iter).c_str(), -1, NULL);
if (suggestion)
suggestions.push_back(suggestion);
}
if (suggestions.empty())
continue;
std::string definition;
for (std::list<char **>::iterator it = suggestions.begin(); it != suggestions.end(); ++it) {
if (it != suggestions.begin()) {
definition += "\n\n";
}
suggestion = *it;
definition += "<kref>";
definition += suggestion[0];
definition += "</kref>";
int i = 1;
while (suggestion[i]) {
definition += "\t<kref>";
definition += suggestion[i];
definition += "</kref>";
i++;
}
}
result.push_back(std::pair<char *, char *>(g_strdup((*iter).c_str()), plugin_service->build_dictdata('x', definition.c_str())));
}
if (result.empty()) {
*pppWord = NULL;
} else {
int head;
if (underline_str.empty())
head = 0;
else
head = 1;
*pppWord = (gchar **)g_malloc(sizeof(gchar *)*(result.size()+1+head));
*ppppWordData = (gchar ***)g_malloc(sizeof(gchar **)*(result.size()+head));
if (head) {
(*pppWord)[0] = g_strdup(text);
(*ppppWordData)[0] = (gchar **)g_malloc(sizeof(gchar *)*2);
(*ppppWordData)[0][0] = plugin_service->build_dictdata('g', underline_str.c_str());
(*ppppWordData)[0][1] = NULL;
}
for (std::vector< std::pair<char *, char *> >::size_type i = 0; i< result.size(); i++) {
(*pppWord)[i+head] = result[i].first;
(*ppppWordData)[i+head] = (gchar **)g_malloc(sizeof(gchar *)*2);
(*ppppWordData)[i+head][0] = result[i].second;
(*ppppWordData)[i+head][1] = NULL;
}
(*pppWord)[result.size()+head] = NULL;
}
}
static bool load_custom_langs()
{
for (std::list<EnchantDict *>::iterator i = dictlist.begin(); i != dictlist.end(); ++i) {
enchant_broker_free_dict(broker, *i);
}
dictlist.clear();
std::list<std::string> langlist;
std::string lang;
const gchar *p = custom_langs.c_str();
const gchar *p1;
while (true) {
p1 = strchr(p, ' ');
if (!p1)
break;
lang.assign(p, p1-p);
langlist.push_back(lang);
p = p1 + 1;
}
lang = p;
langlist.push_back(lang);
EnchantDict *dict;
for (std::list<std::string>::iterator i = langlist.begin(); i != langlist.end(); ++i) {
dict = enchant_broker_request_dict(broker, i->c_str());
if (dict) {
dictlist.push_back(dict);
g_print(_("Find %s spellchecking dictionary!\n"), i->c_str());
} else {
g_print(_("Warning: failure when requesting a spellchecking dictionary for %s language.\n"), i->c_str());
}
}
if (dictlist.empty()) {
enchant_broker_free(broker);
broker = NULL;
g_print(_("Error, no spellchecking dictionary available!\n"));
return true;
} else {
return false;
}
}
static bool load_auto_lang()
{
for (std::list<EnchantDict *>::iterator i = dictlist.begin(); i != dictlist.end(); ++i) {
enchant_broker_free_dict(broker, *i);
}
dictlist.clear();
bool no_dict = false;
const gchar* const *languages = g_get_language_names();
int i = 0;
while (true) {
if (languages[i]==NULL) {
no_dict = true;
break;
}
if (strchr(languages[i], '.') == NULL) {
if (enchant_broker_dict_exists(broker, languages[i]))
break;
}
i++;
}
bool found;
EnchantDict *dict;
if (no_dict) {
if (enchant_broker_dict_exists(broker, "en_US") && ((dict = enchant_broker_request_dict(broker, "en_US"))!=NULL)) {
g_print(_("Find en_US spellchecking dictionary!\n"));
found = true;
} else {
found = false;
}
} else {
if ((dict = enchant_broker_request_dict(broker, languages[i]))!=NULL) {
g_print(_("Find %s spellchecking dictionary!\n"), languages[i]);
found = true;
} else {
found = false;
}
}
if (!found) {
enchant_broker_free(broker);
broker = NULL;
g_print(_("Error, no spellchecking dictionary available!\n"));
return true;
} else {
dictlist.push_back(dict);
return false;
}
}
static void on_use_custom_ckbutton_toggled(GtkToggleButton *button, GtkWidget *hbox)
{
gtk_widget_set_sensitive(hbox, gtk_toggle_button_get_active(button));
}
static void configure()
{
GtkWidget *window = gtk_dialog_new_with_buttons(_("Spell check configuration"), GTK_WINDOW(plugin_info->pluginwin), GTK_DIALOG_MODAL, GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, NULL);
#if GTK_MAJOR_VERSION >= 3
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
#else
GtkWidget *vbox = gtk_vbox_new(false, 5);
#endif
GtkWidget *check_button = gtk_check_button_new_with_mnemonic(_("_Use custom languages."));
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check_button), use_custom);
gtk_box_pack_start(GTK_BOX(vbox), check_button, false, false, 0);
GtkWidget *label = gtk_label_new(_("For example: \"en_US de\""));
gtk_box_pack_start(GTK_BOX(vbox), label, false, false, 0);
#if GTK_MAJOR_VERSION >= 3
GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
#else
GtkWidget *hbox = gtk_hbox_new(FALSE, 5);
#endif
gtk_widget_set_sensitive(hbox, use_custom);
g_signal_connect(G_OBJECT(check_button), "toggled", G_CALLBACK(on_use_custom_ckbutton_toggled), hbox);
gtk_box_pack_start(GTK_BOX(vbox), hbox, false, false, 0);
label = gtk_label_new(_("Custom languages:"));
gtk_box_pack_start(GTK_BOX(hbox), label, false, false, 0);
GtkWidget *entry = gtk_entry_new();
gtk_entry_set_text(GTK_ENTRY(entry), custom_langs.c_str());
gtk_box_pack_start(GTK_BOX(hbox), entry, false, false, 0);
std::string language_names;
const gchar * const * lang_names = g_get_language_names ();
int i = 0;
while (true) {
if (i == 0) {
} else {
language_names += ' ';
}
if (lang_names[i]==NULL) {
break;
} else {
language_names += lang_names[i];
}
i++;
}
label = gtk_label_new(language_names.c_str());
gtk_label_set_selectable(GTK_LABEL(label), true);
gtk_box_pack_start(GTK_BOX(vbox), label, false, false, 0);
gtk_widget_show_all(vbox);
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area(GTK_DIALOG(window))), vbox);
gtk_dialog_run(GTK_DIALOG(window));
gboolean new_use_custom = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(check_button));
bool cfgchanged = false;
if (new_use_custom != use_custom) {
cfgchanged = true;
use_custom = new_use_custom;
custom_langs = gtk_entry_get_text(GTK_ENTRY(entry));
if (use_custom) {
load_custom_langs();
} else {
load_auto_lang();
}
} else {
if (use_custom == true) {
const gchar *str = gtk_entry_get_text(GTK_ENTRY(entry));
if (custom_langs != str) {
cfgchanged = true;
custom_langs = str;
load_custom_langs();
}
}
}
if (cfgchanged) {
const char *tmp;
if (use_custom)
tmp = "true";
else
tmp = "false";
gchar *data = g_strdup_printf("[spell]\nuse_custom=%s\ncustom_langs=%s\n", tmp, custom_langs.c_str());
std::string res = get_cfg_filename();
g_file_set_contents(res.c_str(), data, -1, NULL);
g_free(data);
}
gtk_widget_destroy (window);
}
bool stardict_plugin_init(StarDictPlugInObject *obj, IAppDirs* appDirs)
{
g_debug(_("Loading Spelling plug-in..."));
if (strcmp(obj->version_str, PLUGIN_SYSTEM_VERSION)!=0) {
g_print(_("Error: Spell plugin version doesn't match!\n"));
return true;
}
obj->type = StarDictPlugInType_VIRTUALDICT;
obj->info_xml = g_strdup_printf("<plugin_info><name>%s</name><version>1.0</version><short_desc>%s</short_desc><long_desc>%s</long_desc><author>Hu Zheng <huzheng001@gmail.com></author><website>http://stardict-4.sourceforge.net</website></plugin_info>", _("Spell Check"), _("Spell check virtual dictionary."), _("Spell check the input words and show the correct suggestion."));
obj->configure_func = configure;
plugin_info = obj->plugin_info;
plugin_service = obj->plugin_service;
gpAppDirs = appDirs;
return false;
}
void stardict_plugin_exit(void)
{
if (broker) {
for (std::list<EnchantDict *>::iterator i = dictlist.begin(); i != dictlist.end(); ++i) {
enchant_broker_free_dict(broker, *i);
}
enchant_broker_free(broker);
}
if (layout) {
g_object_unref(layout);
}
gpAppDirs = NULL;
}
bool stardict_virtualdict_plugin_init(StarDictVirtualDictPlugInObject *obj)
{
obj->lookup_func = lookup;
obj->dict_name = _("Spelling Suggestion");
obj->author = _("Hu Zheng");
obj->email = _("huzheng001@gmail.com");
obj->website = _("http://www.huzheng.org");
obj->date = _("2023.12.22");
broker = enchant_broker_init();
layout = pango_layout_new(gtk_widget_get_pango_context(plugin_info->mainwin));
std::string res = get_cfg_filename();
if (!g_file_test(res.c_str(), G_FILE_TEST_EXISTS)) {
use_custom = false;
custom_langs.clear();
g_file_set_contents(res.c_str(), "[spell]\nuse_custom=false\ncustom_langs=\n", -1, NULL);
} else {
GKeyFile *keyfile = g_key_file_new();
g_key_file_load_from_file(keyfile, res.c_str(), G_KEY_FILE_NONE, NULL);
GError *err = NULL;
use_custom = g_key_file_get_boolean(keyfile, "spell", "use_custom", &err);
if (err) {
g_error_free (err);
use_custom = false;
}
gchar *str = g_key_file_get_string(keyfile, "spell", "custom_langs", NULL);
if (str) {
custom_langs = str;
g_free(str);
}
g_key_file_free(keyfile);
}
bool failed;
if (use_custom && !custom_langs.empty()) {
failed = load_custom_langs();
} else {
failed = load_auto_lang();
}
if (failed)
return true;
g_print(_("Spelling plug-in \033[31m[loaded]\033[0m.\n"));
return false;
}