source: branches/1.1dev/lib/SpellCheck.inc.php @ 567

Last change on this file since 567 was 503, checked in by anonymous, 9 years ago

Backported spellcheck fixes

File size: 13.8 KB
Line 
1<?php
2/**
3 * SpellCheck.inc.php
4 * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
5 *
6 * Interface to PHP's pspell functions.
7 *
8 * @author  Quinn Comendant <quinn@strangecode.com>
9 * @version 1.0
10 */
11class SpellCheck {
12
13    // Default parameters.
14    var $_params = array(
15        'language' => 'en',
16        'personal_wordlist' => '',
17        'skip_length' => 3,
18        'mode' => PSPELL_NORMAL, // PSPELL_FAST, PSPELL_NORMAL, or PSPELL_BAD_SPELLERS.
19        'highlight_start' => '<strong style="color:red;">',
20        'highlight_end' => '</strong>',
21    );
22
23    var $_pspell_cfg_handle;
24    var $_pspell_handle;
25    var $_use_personal_wordlist = false;
26    var $_errors = array();
27
28    /**
29     * Constructor.
30     *
31     * @param  array    $params     Array of parameters (key => val pairs).
32     */
33    function SpellCheck($params)
34    {
35        if (!is_array($params) || empty($params)) {
36            trigger_error('SpellCheck parameters not set properly', E_USER_ERROR);
37        }
38
39        $this->setParam($params);
40
41        $this->_pspell_cfg_handle = pspell_config_create($this->getParam('language'));
42
43        pspell_config_ignore($this->_pspell_cfg_handle, $this->getParam('skip_length'));
44        pspell_config_mode($this->_pspell_cfg_handle, $this->getParam('mode'));
45
46        if ('' != $this->getParam('personal_wordlist')) {
47            if (!file_exists($this->getParam('personal_wordlist')) || !is_writable($this->getParam('personal_wordlist'))) {
48                logMsg(sprintf('Personal wordlist file does not exist or is not writable: %s', $this->getParam('personal_wordlist')), LOG_WARNING, __FILE__, __LINE__);
49            } else {
50                pspell_config_personal($this->_pspell_cfg_handle, $this->getParam('personal_wordlist'));
51                $this->_use_personal_wordlist = true;
52                logMsg(sprintf('Using personal wordlist: %s', $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
53            }
54        }
55
56        $this->_pspell_handle = pspell_new_config($this->_pspell_cfg_handle);
57    }
58
59    /**
60     * Set (or overwrite existing) parameters by passing an array of new parameters.
61     *
62     * @access public
63     *
64     * @param  array    $params     Array of parameters (key => val pairs).
65     */
66    function setParam($params=null)
67    {
68        if (isset($params) && is_array($params)) {
69            // Merge new parameters with old overriding only those passed.
70            $this->_params = array_merge($this->_params, $params);
71        } else {
72            logMsg(sprintf('Supplied argument is not an array: %s', $params), LOG_WARNING, __FILE__, __LINE__);
73        }
74    }
75
76    /**
77     * Return the value of a parameter.
78     *
79     * @access  public
80     * @param   string  $param      The key of the parameter to return.
81     * @return  mixed               Parameter value.
82     */
83    function getParam($param)
84    {
85        return $this->_params[$param];
86    }
87
88    /**
89     * Check whether any errors have been triggered.
90     *
91     * @return bool   True if any errors were found, false otherwise.
92     */
93    function anyErrors()
94    {
95        return (sizeof($this->_errors) > 0);
96    }
97
98    /**
99     * Reset the error list.
100     */
101    function resetErrorList()
102    {
103        $this->_errors = array();
104    }
105
106    /**
107     * Check one word.
108     *
109     * @access  public
110     * @param   string  $word
111     * @return  bool    True if word is correct.
112     * @author  Quinn Comendant <quinn@strangecode.com>
113     * @version 1.0
114     * @since   09 Jun 2005 18:23:51
115     */
116    function check($word)
117    {
118        if (pspell_check($this->_pspell_handle, $word)) {
119            return true;
120        } else {
121            $this->_errors[] = $word;
122            return false;
123        }
124    }
125
126    /**
127     * Suggest the correct spelling for one misspelled word.
128     *
129     * @access  public
130     * @param   string  $word
131     * @return  array   Word suggestions.
132     * @author  Quinn Comendant <quinn@strangecode.com>
133     * @version 1.0
134     * @since   09 Jun 2005 18:23:51
135     */
136    function suggest($word)
137    {
138        return pspell_suggest($this->_pspell_handle, $word);
139    }
140
141    /**
142     * Add a word to a personal list.
143     *
144     * @access  public
145     * @param   string  $word
146     * @return  array   Word suggestions.
147     * @author  Quinn Comendant <quinn@strangecode.com>
148     * @version 1.0
149     * @since   09 Jun 2005 18:23:51
150     */
151    function add($word)
152    {
153        if ($this->_use_personal_wordlist) {
154            if (pspell_add_to_personal($this->_pspell_handle, $word)) {
155                logMsg(sprintf('Added "%s" to personal wordlist: %s', $word, $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
156                return true;
157            } else {
158                logMsg(sprintf('Failed adding "%s" to personal wordlist: %s', $word, $this->getParam('personal_wordlist')), LOG_ERR, __FILE__, __LINE__);
159                return false;
160            }
161        }
162    }
163
164    /**
165     * Save personal list to file.
166     *
167     * @access  public
168     * @param   string  $word
169     * @return  array   Word suggestions.
170     * @author  Quinn Comendant <quinn@strangecode.com>
171     * @version 1.0
172     * @since   09 Jun 2005 18:23:51
173     */
174    function save()
175    {
176        if ($this->_use_personal_wordlist) {
177            if (pspell_save_wordlist($this->_pspell_handle)) {
178                logMsg(sprintf('Saved personal wordlist: %s', $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
179                return true;
180            } else {
181                logMsg(sprintf('Failed saving personal wordlist: %s', $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
182                return false;
183            }
184        }
185    }
186
187    /**
188     * Returns an array of suggested words for each misspelled word in the given text.
189     * The first word of the returned array is the (possibly) misspelled word.
190     *
191     * @access  public
192     * @param   string  $string String to get suggestions for.
193     * @return  mixed   Array of suggested words or false if none.
194     * @author  Quinn Comendant <quinn@strangecode.com>
195     * @version 1.0
196     * @since   09 Jun 2005 21:29:49
197     */
198    function getStringSuggestions($string)
199    {
200        $corrections = array();
201        // Split words on punctuation except apostrophes (this regex is used in several places in this class).
202        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
203        $words = preg_split("/((?:^\p{P}+)|(?:\p{P}*\s+\p{P}*)|[\p{Pd}—–-]+|(?:\p{P}+$))/", $string, -1, PREG_SPLIT_DELIM_CAPTURE);
204        if (is_array($words) && !empty($words)) {
205            // Remove non-word elements.
206            $words = preg_grep('/\w+/', $words);
207            $words = array_map('strip_tags', $words);
208            foreach ($words as $i => $word) {
209                if (!$this->check($word)) {
210                    $corrections[$i] = $this->suggest($word);
211                    // Keep the original spelling as one of the suggestions.
212                    array_unshift($corrections[$i], $word);
213                    array_unique($corrections[$i]);
214                }
215            }
216        }
217        if (is_array($corrections) && !empty($corrections)) {
218            return $corrections;
219        } else {
220            return false;
221        }
222    }
223
224    /**
225     * Checks all words in a given string.
226     *
227     * @access  public
228     * @param   string  $string     String to check.
229     * @return  void
230     * @author  Quinn Comendant <quinn@strangecode.com>
231     * @version 1.0
232     * @since   09 Jun 2005 22:11:27
233     */
234    function checkString($string)
235    {
236        $errors = array();
237        // Split words on punctuation except apostrophes (this regex is used in several places in this class).
238        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
239        $words = preg_split("/((?:^\p{P}+)|(?:\p{P}*\s+\p{P}*)|[\p{Pd}—–-]+|(?:\p{P}+$))/", $string, -1, PREG_SPLIT_DELIM_CAPTURE);
240        if (is_array($words) && !empty($words)) {
241            // Remove non-word elements.
242            $words = preg_grep('/\w+/', $words);
243            $words = array_map('strip_tags', $words);
244            foreach ($words as $i => $word) {
245                if (!$this->check($word)) {
246                    $errors[] = $word;
247                }
248            }
249        }
250        if (empty($errors)) {
251            return true;
252        } else {
253            $this->_errors = $errors + $this->_errors;
254            return false;
255        }
256    }
257
258    /**
259     * Returns a given string with misspelled words highlighted.
260     *
261     * @access  public
262     * @param   string  $string     Text to highlight.
263     * @return  string  Highlighted text.
264     * @author  Quinn Comendant <quinn@strangecode.com>
265     * @version 1.0
266     * @since   09 Jun 2005 21:29:49
267     */
268    function getStringHighlighted($string, $show_footnote=false)
269    {
270        // Split words on punctuation except apostrophes (this regex is used in several places in this class).
271        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
272        $words = preg_split("/((?:^\p{P}+)|(?:\p{P}*\s+\p{P}*)|[\p{Pd}—–-]+|(?:\p{P}+$))/", $string, -1, PREG_SPLIT_DELIM_CAPTURE);
273        $cnt = 0;
274        if (is_array($words) && !empty($words)) {
275            $words = preg_grep('/\w+/', $words);
276            $words = array_map('strip_tags', $words);
277            foreach ($words as $i => $word) {
278                if (!$this->check($word)) {
279                    $footnote = $show_footnote ? '<sup style="color:#999;">' . ++$cnt . '</sup>' : '';
280                    $words[$i] = $this->getParam('highlight_start') . $word . $this->getParam('highlight_end') . $footnote;
281                    $string = preg_replace(sprintf('/\b%s\b/', preg_quote($word, '/')), $words[$i], $string);
282                }
283            }
284        }
285        return $string;
286    }
287
288    /**
289     * Prints the HTML for correcting all misspellings found in the text of one $_FORM element.
290     *
291     * @access  public
292     * @param   string  $form_name  Name of the form to check.
293     * @return  void
294     * @author  Quinn Comendant <quinn@strangecode.com>
295     * @version 1.0
296     * @since   09 Jun 2005 21:29:49
297     */
298    function printCorrectionForm($form_name)
299    {
300        ?>
301        <input name="<?php echo $form_name ?>" type="hidden" value="<?php echo oTxt(getFormData($form_name)) ?>" />
302        <?php
303
304        $form_words = $this->getStringSuggestions(getFormData($form_name));
305        if (is_array($form_words) && !empty($form_words)) {
306            ?><ol><?php
307            $j = 1;
308            foreach ($form_words as $i => $words) {
309                ?>
310                <li>
311                <label style="color:#999;"><sub style="vertical-align:text-top;"><?php echo $j++; ?></sub></label>
312                <select name="spelling_suggestions[<?php echo $form_name ?>][<?php echo $i ?>]" onchange="document.forms[0].elements['spelling_corrections[<?php echo $form_name ?>][<?php echo $i ?>]'].value = this.value;">
313                <?php $original_word = array_shift($words); ?>
314                <option value="<?php echo $original_word ?>">(<?php echo $original_word ?>)</option>
315                <?php
316
317                foreach ($words as $suggestion) {
318                    ?>
319                    <option value="<?php echo $suggestion ?>"><?php echo $suggestion ?></option>
320                    <?php
321                }
322
323                ?>
324                </select>
325                <input type="text" name="spelling_corrections[<?php echo $form_name ?>][<?php echo $i ?>]" value="<?php echo oTxt($original_word) ?>" size="20">
326                <?php if ($this->_use_personal_wordlist) { ?>
327                <input name="save_to_personal_wordlist[]" type="checkbox" value="<?php echo $i ?>" /><?php echo _("Learn spelling") ?>
328                <?php } ?>
329                </li>
330                <?php
331            }
332            ?></ol><?php
333        }
334    }
335
336    /**
337     * Tests if any form spelling corrections have been submitted.
338     *
339     * @access  public
340     * @return  bool    True if form spelling has been checked.
341     * @author  Quinn Comendant <quinn@strangecode.com>
342     * @version 1.0
343     * @since   09 Jun 2005 23:15:35
344     */
345    function anyFormCorrections()
346    {
347        return (false !== getFormData('spelling_suggestions', false)) || (false !== getFormData('spelling_corrections', false));
348    }
349
350    /**
351     * Replace the misspelled words in the text of a specified form with the corrections.
352     *
353     * @access  public
354     * @param   string  $form_name      Name of form to apply corrections to.
355     * @return  string  Corrected form text.
356     * @author  Quinn Comendant <quinn@strangecode.com>
357     * @version 1.0
358     * @since   09 Jun 2005 23:18:51
359     */
360    function applyFormCorrections($form_name)
361    {
362        // Split words on punctuation except apostrophes (this regex is used in several places in this class).
363        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
364        $form_words = preg_split("/((?:^\p{P}+)|(?:\p{P}*\s+\p{P}*)|[\p{Pd}—–-]+|(?:\p{P}+$))/", getFormData($form_name), -1, PREG_SPLIT_DELIM_CAPTURE);
365        $suggestions = getFormData('spelling_suggestions');
366        $corrections = getFormData('spelling_corrections');
367
368        $form_words = array_diff($corrections[$form_name], array('')) + $suggestions[$form_name] + $form_words;
369        ksort($form_words);
370
371        if ($this->_use_personal_wordlist) {
372            $save_to_personal_wordlist = getFormData('save_to_personal_wordlist');
373            if (is_array($save_to_personal_wordlist) && !empty($save_to_personal_wordlist)) {
374                foreach ($save_to_personal_wordlist as $cust) {
375                    $this->add($form_words[$cust]);
376                }
377            }
378            $this->save();
379        }
380
381        if (is_array($form_words) && !empty($form_words)) {
382            return join('', $form_words);
383        } else {
384            return getFormData($form_name);
385        }
386    }
387
388} // End.
389
390?>
Note: See TracBrowser for help on using the repository browser.