1<?php
2
3/**
4 +-----------------------------------------------------------------------+
5 | This file is part of the Roundcube Webmail client                     |
6 |                                                                       |
7 | Copyright (C) The Roundcube Dev Team                                  |
8 | Copyright (C) Kolab Systems AG                                        |
9 |                                                                       |
10 | Licensed under the GNU General Public License version 3 or            |
11 | any later version with exceptions for skins & plugins.                |
12 | See the README file for a full license statement.                     |
13 |                                                                       |
14 | PURPOSE:                                                              |
15 |   Spellchecking backend implementation to work with Enchant           |
16 +-----------------------------------------------------------------------+
17 | Author: Aleksander Machniak <machniak@kolabsys.com>                   |
18 +-----------------------------------------------------------------------+
19*/
20
21/**
22 * Spellchecking backend implementation to work with Pspell
23 *
24 * @package    Framework
25 * @subpackage Utils
26 */
27class rcube_spellchecker_enchant extends rcube_spellchecker_engine
28{
29    private $enchant_broker;
30    private $enchant_dictionary;
31    private $matches = [];
32
33    /**
34     * Return a list of languages supported by this backend
35     *
36     * @see rcube_spellchecker_engine::languages()
37     */
38    function languages()
39    {
40        $this->init();
41
42        if (!$this->enchant_broker) {
43            return [];
44        }
45
46        $langs = [];
47        if ($dicts = enchant_broker_list_dicts($this->enchant_broker)) {
48            foreach ($dicts as $dict) {
49                $langs[] = preg_replace('/-.*$/', '', $dict['lang_tag']);
50            }
51        }
52
53        return array_unique($langs);
54    }
55
56    /**
57     * Initializes Enchant dictionary
58     */
59    private function init()
60    {
61        if (!$this->enchant_broker) {
62            if (!extension_loaded('enchant')) {
63                $this->error = "Enchant extension not available";
64                return;
65            }
66
67            $this->enchant_broker = enchant_broker_init();
68        }
69
70        if (!enchant_broker_dict_exists($this->enchant_broker, $this->lang)) {
71            $this->error = "Unable to load dictionary for selected language using Enchant";
72            return;
73        }
74
75        $this->enchant_dictionary = enchant_broker_request_dict($this->enchant_broker, $this->lang);
76    }
77
78    /**
79     * Set content and check spelling
80     *
81     * @see rcube_spellchecker_engine::check()
82     */
83    function check($text)
84    {
85        $this->init();
86
87        if (!$this->enchant_dictionary) {
88            return [];
89        }
90
91        // tokenize
92        $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
93
94        $diff    = 0;
95        $matches = [];
96
97        foreach ($text as $w) {
98            $word = trim($w[0]);
99            $pos  = $w[1] - $diff;
100            $len  = mb_strlen($word);
101
102            if ($this->dictionary->is_exception($word)) {
103                // skip exceptions
104            }
105            else if (!enchant_dict_check($this->enchant_dictionary, $word)) {
106                $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
107
108                if (is_array($suggestions) && count($suggestions) > self::MAX_SUGGESTIONS) {
109                    $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
110                }
111
112                $matches[] = [$word, $pos, $len, null, $suggestions];
113            }
114
115            $diff += (strlen($word) - $len);
116        }
117
118        $this->matches = $matches;
119        return $matches;
120    }
121
122    /**
123     * Returns suggestions for the specified word
124     *
125     * @see rcube_spellchecker_engine::get_words()
126     */
127    function get_suggestions($word)
128    {
129        $this->init();
130
131        if (!$this->enchant_dictionary) {
132            return [];
133        }
134
135        $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
136
137        if (is_array($suggestions) && count($suggestions) > self::MAX_SUGGESTIONS) {
138            $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
139        }
140
141        return is_array($suggestions) ? $suggestions : [];
142    }
143
144    /**
145     * Returns misspelled words
146     *
147     * @see rcube_spellchecker_engine::get_suggestions()
148     */
149    function get_words($text = null)
150    {
151        $result = [];
152
153        if ($text) {
154            // init spellchecker
155            $this->init();
156
157            if (!$this->enchant_dictionary) {
158                return [];
159            }
160
161            // With Enchant we don't need to get suggestions to return misspelled words
162            $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
163
164            foreach ($text as $w) {
165                $word = trim($w[0]);
166
167                // skip exceptions
168                if ($this->dictionary->is_exception($word)) {
169                    continue;
170                }
171
172                if (!enchant_dict_check($this->enchant_dictionary, $word)) {
173                    $result[] = $word;
174                }
175            }
176
177            return $result;
178        }
179
180        foreach ($this->matches as $m) {
181            $result[] = $m[0];
182        }
183
184        return $result;
185    }
186}
187