source: trunk/lib/SpellCheck.inc.php @ 558

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

Backported spellcheck fixes

File size: 16.4 KB
RevLine 
[1]1<?php
2/**
[362]3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
[396]5 * Copyright 2001-2012 Strangecode, LLC
[503]6 *
[362]7 * This file is part of The Strangecode Codebase.
8 *
9 * The Strangecode Codebase is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
[503]13 *
[362]14 * The Strangecode Codebase is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
[503]18 *
[362]19 * You should have received a copy of the GNU General Public License along with
20 * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23/**
[1]24 * SpellCheck.inc.php
25 *
26 * Interface to PHP's pspell functions.
27 *
28 * @author  Quinn Comendant <quinn@strangecode.com>
[15]29 * @version 1.1
[1]30 */
[42]31
[1]32/* Implementation example:
33--------------------------------------------------------------------------------
[15]34include_once dirname(__FILE__) . '/_config.inc.php';
[1]35include 'codebase/lib/SpellCheck.inc.php';
36
[15]37// Instantiate with parameters. In this example we'll set the language and the path to the personal wordlist file.
38$spell = new SpellCheck(array(
[42]39    'language' => 'en',
[15]40    'personal_wordlist' => '/tmp/my_custom_dict'
41));
[1]42
[15]43// Just for the heck of it add a new word to persistent personal wordlist file.
44$spell->add('mealworm');
45
[1]46$text_to_check = 'donky rinds taste like mealworm paste';
47
48if (!$spell->checkString($text_to_check)) {
49    $suggestions = $spell->getStringSuggestions($text_to_check);
[15]50    echo 'Spelling errors! Here are suggested alternatives:';
[1]51    print_r($suggestions);
52} else {
53    echo 'No spelling errors';
54}
55
56// Save added words to persistent custom wordlist file.
57$spell->save();
58--------------------------------------------------------------------------------
59*/
60
[502]61class SpellCheck
62{
[1]63
[484]64    protected $_params = array(
[15]65        'language' => 'en',
66        'personal_wordlist' => '', // Text file to save custom words to.
67        'skip_length' => 3, // Words with this many chars or less will not be checked.
[1]68        'mode' => PSPELL_NORMAL, // PSPELL_FAST, PSPELL_NORMAL, or PSPELL_BAD_SPELLERS.
69        'highlight_start' => '<strong style="color:red;">',
70        'highlight_end' => '</strong>',
71    );
[42]72
[484]73    protected $_pspell_cfg_handle;
74    protected $_pspell_handle;
75    protected $_use_personal_wordlist = false;
76    protected $_errors = array();
[42]77
[1]78    /**
79     * Constructor.
[42]80     *
[15]81     * @param  array    $params     Array of parameters (key => val pairs).
[1]82     */
[468]83    public function __construct($params)
[1]84    {
[479]85        $app =& App::getInstance();
[136]86
[275]87        if (!extension_loaded('pspell')) {
88            trigger_error('Pspell module not installed', E_USER_ERROR);
89        }
90
[42]91        if (!is_array($params) || empty($params)) {
92            trigger_error('SpellCheck parameters not set properly', E_USER_ERROR);
93        }
[1]94
[42]95        $this->setParam($params);
[1]96
[15]97        $this->_pspell_cfg_handle = pspell_config_create($this->getParam('language'));
98
99        pspell_config_ignore($this->_pspell_cfg_handle, $this->getParam('skip_length'));
100        pspell_config_mode($this->_pspell_cfg_handle, $this->getParam('mode'));
101
102        if ('' != $this->getParam('personal_wordlist')) {
103            if (!is_writable(dirname($this->getParam('personal_wordlist'))) || !is_writable($this->getParam('personal_wordlist'))) {
[136]104                $app->logMsg(sprintf('Personal wordlist file not writable: %s', $this->getParam('personal_wordlist')), LOG_WARNING, __FILE__, __LINE__);
[1]105            } else {
[15]106                pspell_config_personal($this->_pspell_cfg_handle, $this->getParam('personal_wordlist'));
[1]107                $this->_use_personal_wordlist = true;
[136]108                $app->logMsg(sprintf('Using personal wordlist: %s', $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
[1]109            }
110        }
111
112        $this->_pspell_handle = pspell_new_config($this->_pspell_cfg_handle);
113    }
114
115    /**
116     * Set (or overwrite existing) parameters by passing an array of new parameters.
117     *
118     * @access public
119     * @param  array    $params     Array of parameters (key => val pairs).
120     */
[468]121    public function setParam($params)
[1]122    {
[479]123        $app =& App::getInstance();
[136]124
[1]125        if (isset($params) && is_array($params)) {
126            // Merge new parameters with old overriding only those passed.
127            $this->_params = array_merge($this->_params, $params);
128        } else {
[136]129            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
[1]130        }
131    }
132
133    /**
134     * Return the value of a parameter, if it exists.
135     *
136     * @access public
137     * @param string $param        Which parameter to return.
138     * @return mixed               Configured parameter value.
139     */
[468]140    public function getParam($param)
[1]141    {
[479]142        $app =& App::getInstance();
[503]143
[478]144        if (array_key_exists($param, $this->_params)) {
[1]145            return $this->_params[$param];
146        } else {
[146]147            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
[1]148            return null;
149        }
150    }
[42]151
[1]152    /**
153     * Check whether any errors have been triggered.
154     *
155     * @return bool   True if any errors were found, false otherwise.
156     */
[468]157    public function anyErrors()
[1]158    {
159        return (sizeof($this->_errors) > 0);
160    }
161
162    /**
163     * Reset the error list.
164     */
[468]165    public function resetErrorList()
[1]166    {
167        $this->_errors = array();
168    }
[42]169
[1]170    /**
171     * Check one word.
172     *
173     * @access  public
174     * @param   string  $word
175     * @return  bool    True if word is correct.
176     * @author  Quinn Comendant <quinn@strangecode.com>
177     * @version 1.0
178     * @since   09 Jun 2005 18:23:51
179     */
[468]180    public function check($word)
[1]181    {
182        if (pspell_check($this->_pspell_handle, $word)) {
183            return true;
184        } else {
185            $this->_errors[] = $word;
186            return false;
187        }
188    }
[42]189
[1]190    /**
191     * Suggest the correct spelling for one misspelled word.
192     *
193     * @access  public
194     * @param   string  $word
195     * @return  array   Word suggestions.
196     * @author  Quinn Comendant <quinn@strangecode.com>
197     * @version 1.0
198     * @since   09 Jun 2005 18:23:51
199     */
[468]200    public function suggest($word)
[1]201    {
202        return pspell_suggest($this->_pspell_handle, $word);
203    }
[42]204
[1]205    /**
206     * Add a word to a personal list.
207     *
208     * @access  public
209     * @param   string  $word
210     * @return  array   Word suggestions.
211     * @author  Quinn Comendant <quinn@strangecode.com>
212     * @version 1.0
213     * @since   09 Jun 2005 18:23:51
214     */
[468]215    public function add($word)
[1]216    {
[479]217        $app =& App::getInstance();
[136]218
[1]219        if ($this->_use_personal_wordlist) {
[15]220            if (pspell_add_to_personal($this->_pspell_handle, $word)) {
[136]221                $app->logMsg(sprintf('Added "%s" to personal wordlist: %s', $word, $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
[42]222                return true;
223            } else {
[136]224                $app->logMsg(sprintf('Failed adding "%s" to personal wordlist: %s', $word, $this->getParam('personal_wordlist')), LOG_WARNING, __FILE__, __LINE__);
[42]225                return false;
226            }
[1]227        }
228    }
[42]229
[1]230    /**
231     * Save personal list to file.
232     *
233     * @access  public
234     * @param   string  $word
235     * @return  array   Word suggestions.
236     * @author  Quinn Comendant <quinn@strangecode.com>
237     * @version 1.0
238     * @since   09 Jun 2005 18:23:51
239     */
[468]240    public function save()
[1]241    {
[479]242        $app =& App::getInstance();
[136]243
[1]244        if ($this->_use_personal_wordlist) {
245            if (pspell_save_wordlist($this->_pspell_handle)) {
[136]246                $app->logMsg(sprintf('Saved personal wordlist: %s', $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
[1]247                return true;
248            } else {
[136]249                $app->logMsg(sprintf('Failed saving personal wordlist: %s', $this->getParam('personal_wordlist')), LOG_ERR, __FILE__, __LINE__);
[1]250                return false;
251            }
252        }
253    }
[42]254
[1]255    /**
[334]256     * Returns an array of suggested words for each misspelled word in the given text.
[1]257     * The first word of the returned array is the (possibly) misspelled word.
258     *
259     * @access  public
260     * @param   string  $string String to get suggestions for.
261     * @return  mixed   Array of suggested words or false if none.
262     * @author  Quinn Comendant <quinn@strangecode.com>
263     * @version 1.0
264     * @since   09 Jun 2005 21:29:49
265     */
[468]266    public function getStringSuggestions($string)
[1]267    {
268        $corrections = array();
[503]269        // Split words on punctuation except apostrophes (this regex is used in several places in this class).
[425]270        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
[503]271        $words = preg_split("/((?:^\p{P}+)|(?:\p{P}*\s+\p{P}*)|[\p{Pd}—–-]+|(?:\p{P}+$))/", $string, -1, PREG_SPLIT_DELIM_CAPTURE);
[1]272        if (is_array($words) && !empty($words)) {
[503]273            // Remove non-word elements.
274            $words = preg_grep('/\w+/', $words);
275            $words = array_map('strip_tags', $words);
[1]276            foreach ($words as $i => $word) {
277                if (!$this->check($word)) {
278                    $corrections[$i] = $this->suggest($word);
279                    // Keep the original spelling as one of the suggestions.
280                    array_unshift($corrections[$i], $word);
281                    array_unique($corrections[$i]);
282                }
283            }
284        }
285        if (is_array($corrections) && !empty($corrections)) {
286            return $corrections;
287        } else {
288            return false;
289        }
290    }
[42]291
[1]292    /**
293     * Checks all words in a given string.
294     *
295     * @access  public
296     * @param   string  $string     String to check.
297     * @return  void
298     * @author  Quinn Comendant <quinn@strangecode.com>
299     * @version 1.0
300     * @since   09 Jun 2005 22:11:27
301     */
[468]302    public function checkString($string)
[1]303    {
304        $errors = array();
[503]305        // Split words on punctuation except apostrophes (this regex is used in several places in this class).
306        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
307        $words = preg_split("/((?:^\p{P}+)|(?:\p{P}*\s+\p{P}*)|[\p{Pd}—–-]+|(?:\p{P}+$))/", $string, -1, PREG_SPLIT_DELIM_CAPTURE);
308        if (is_array($words) && !empty($words)) {
309            // Remove non-word elements.
310            $words = preg_grep('/\w+/', $words);
311            $words = array_map('strip_tags', $words);
312            foreach ($words as $i => $word) {
[1]313                if (!$this->check($word)) {
314                    $errors[] = $word;
315                }
316            }
317        }
318        if (empty($errors)) {
319            return true;
320        } else {
321            $this->_errors = $errors + $this->_errors;
322            return false;
323        }
324    }
[42]325
[1]326    /**
327     * Returns a given string with misspelled words highlighted.
328     *
329     * @access  public
330     * @param   string  $string     Text to highlight.
331     * @return  string  Highlighted text.
332     * @author  Quinn Comendant <quinn@strangecode.com>
333     * @version 1.0
334     * @since   09 Jun 2005 21:29:49
335     */
[468]336    public function getStringHighlighted($string, $show_footnote=false)
[1]337    {
[503]338        // Split words on punctuation except apostrophes (this regex is used in several places in this class).
339        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
340        $words = preg_split("/((?:^\p{P}+)|(?:\p{P}*\s+\p{P}*)|[\p{Pd}—–-]+|(?:\p{P}+$))/", $string, -1, PREG_SPLIT_DELIM_CAPTURE);
[1]341        $cnt = 0;
[503]342        if (is_array($words) && !empty($words)) {
343            $words = preg_grep('/\w+/', $words);
344            $words = array_map('strip_tags', $words);
345            foreach ($words as $i => $word) {
[1]346                if (!$this->check($word)) {
[503]347                    $footnote = $show_footnote ? '<sup style="color:#999;">' . ++$cnt . '</sup>' : '';
348                    $words[$i] = $this->getParam('highlight_start') . $word . $this->getParam('highlight_end') . $footnote;
349                    $string = preg_replace(sprintf('/\b%s\b/', preg_quote($word, '/')), $words[$i], $string);
[1]350                }
351            }
352        }
[503]353        return $string;
[1]354    }
[42]355
[1]356    /**
[334]357     * Prints the HTML for correcting all misspellings found in the text of one $_FORM element.
[1]358     *
359     * @access  public
360     * @param   string  $form_name  Name of the form to check.
361     * @return  void
362     * @author  Quinn Comendant <quinn@strangecode.com>
363     * @version 1.0
364     * @since   09 Jun 2005 21:29:49
365     */
[468]366    public function printCorrectionForm($form_name)
[1]367    {
368        ?>
[503]369        <input name="<?php echo $form_name ?>" type="hidden" value="<?php echo oTxt(getFormData($form_name)) ?>" />
[1]370        <?php
[42]371
[1]372        $form_words = $this->getStringSuggestions(getFormData($form_name));
373        if (is_array($form_words) && !empty($form_words)) {
374            ?><ol><?php
375            foreach ($form_words as $i => $words) {
376                ?>
377                <li>
[503]378                <label style="color:#999;"><sub style="vertical-align:text-top;"><?php echo $j++; ?></sub></label>
379                <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;">
[1]380                <?php $original_word = array_shift($words); ?>
381                <option value="<?php echo $original_word ?>">(<?php echo $original_word ?>)</option>
382                <?php
[42]383
[1]384                foreach ($words as $suggestion) {
385                    ?>
386                    <option value="<?php echo $suggestion ?>"><?php echo $suggestion ?></option>
387                    <?php
388                }
[42]389
[1]390                ?>
391                </select>
[503]392                <input type="text" name="spelling_corrections[<?php echo $form_name ?>][<?php echo $i ?>]" value="<?php echo oTxt($original_word) ?>" size="20">
[1]393                <?php if ($this->_use_personal_wordlist) { ?>
394                <input name="save_to_personal_wordlist[]" type="checkbox" value="<?php echo $i ?>" /><?php echo _("Learn spelling") ?>
395                <?php } ?>
396                </li>
397                <?php
398            }
399            ?></ol><?php
400        }
401    }
[42]402
[1]403    /**
404     * Tests if any form spelling corrections have been submitted.
405     *
406     * @access  public
407     * @return  bool    True if form spelling has been checked.
408     * @author  Quinn Comendant <quinn@strangecode.com>
409     * @version 1.0
410     * @since   09 Jun 2005 23:15:35
411     */
[468]412    public function anyFormCorrections()
[1]413    {
414        return (false !== getFormData('spelling_suggestions', false)) || (false !== getFormData('spelling_corrections', false));
415    }
[42]416
[1]417    /**
418     * Replace the misspelled words in the text of a specified form with the corrections.
419     *
420     * @access  public
421     * @param   string  $form_name      Name of form to apply corrections to.
422     * @return  string  Corrected form text.
423     * @author  Quinn Comendant <quinn@strangecode.com>
424     * @version 1.0
425     * @since   09 Jun 2005 23:18:51
426     */
[468]427    public function applyFormCorrections($form_name)
[1]428    {
[503]429        // Split words on punctuation except apostrophes (this regex is used in several places in this class).
430        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
431        $form_words = preg_split("/((?:^\p{P}+)|(?:\p{P}*\s+\p{P}*)|[\p{Pd}—–-]+|(?:\p{P}+$))/", getFormData($form_name), -1, PREG_SPLIT_DELIM_CAPTURE);
[1]432        $suggestions = getFormData('spelling_suggestions');
433        $corrections = getFormData('spelling_corrections');
434
435        $form_words = array_diff($corrections[$form_name], array('')) + $suggestions[$form_name] + $form_words;
436        ksort($form_words);
437
438        if ($this->_use_personal_wordlist) {
439            $save_to_personal_wordlist = getFormData('save_to_personal_wordlist');
440            if (is_array($save_to_personal_wordlist) && !empty($save_to_personal_wordlist)) {
441                foreach ($save_to_personal_wordlist as $cust) {
442                    $this->add($form_words[$cust]);
443                }
444            }
445            $this->save();
446        }
447
448        if (is_array($form_words) && !empty($form_words)) {
449            return join('', $form_words);
450        } else {
451            return getFormData($form_name);
452        }
453    }
454
455} // End.
456
Note: See TracBrowser for help on using the repository browser.