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

Last change on this file since 484 was 484, checked in by anonymous, 10 years ago

Changed private methods and properties to protected. A few minor bug fixes.

File size: 15.2 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
[362]6 *
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.
13 *
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.
18 *
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
61class SpellCheck {
62
[484]63    protected $_params = array(
[15]64        'language' => 'en',
65        'personal_wordlist' => '', // Text file to save custom words to.
66        'skip_length' => 3, // Words with this many chars or less will not be checked.
[1]67        'mode' => PSPELL_NORMAL, // PSPELL_FAST, PSPELL_NORMAL, or PSPELL_BAD_SPELLERS.
68        'highlight_start' => '<strong style="color:red;">',
69        'highlight_end' => '</strong>',
70    );
[42]71
[484]72    protected $_pspell_cfg_handle;
73    protected $_pspell_handle;
74    protected $_use_personal_wordlist = false;
75    protected $_errors = array();
[42]76
[1]77    /**
78     * Constructor.
[42]79     *
[15]80     * @param  array    $params     Array of parameters (key => val pairs).
[1]81     */
[468]82    public function __construct($params)
[1]83    {
[479]84        $app =& App::getInstance();
[136]85
[275]86        if (!extension_loaded('pspell')) {
87            trigger_error('Pspell module not installed', E_USER_ERROR);
88        }
89
[42]90        if (!is_array($params) || empty($params)) {
91            trigger_error('SpellCheck parameters not set properly', E_USER_ERROR);
92        }
[1]93
[42]94        $this->setParam($params);
[1]95
[15]96        $this->_pspell_cfg_handle = pspell_config_create($this->getParam('language'));
97
98        pspell_config_ignore($this->_pspell_cfg_handle, $this->getParam('skip_length'));
99        pspell_config_mode($this->_pspell_cfg_handle, $this->getParam('mode'));
100
101        if ('' != $this->getParam('personal_wordlist')) {
102            if (!is_writable(dirname($this->getParam('personal_wordlist'))) || !is_writable($this->getParam('personal_wordlist'))) {
[136]103                $app->logMsg(sprintf('Personal wordlist file not writable: %s', $this->getParam('personal_wordlist')), LOG_WARNING, __FILE__, __LINE__);
[1]104            } else {
[15]105                pspell_config_personal($this->_pspell_cfg_handle, $this->getParam('personal_wordlist'));
[1]106                $this->_use_personal_wordlist = true;
[136]107                $app->logMsg(sprintf('Using personal wordlist: %s', $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
[1]108            }
109        }
110
111        $this->_pspell_handle = pspell_new_config($this->_pspell_cfg_handle);
112    }
113
114    /**
115     * Set (or overwrite existing) parameters by passing an array of new parameters.
116     *
117     * @access public
118     * @param  array    $params     Array of parameters (key => val pairs).
119     */
[468]120    public function setParam($params)
[1]121    {
[479]122        $app =& App::getInstance();
[136]123
[1]124        if (isset($params) && is_array($params)) {
125            // Merge new parameters with old overriding only those passed.
126            $this->_params = array_merge($this->_params, $params);
127        } else {
[136]128            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
[1]129        }
130    }
131
132    /**
133     * Return the value of a parameter, if it exists.
134     *
135     * @access public
136     * @param string $param        Which parameter to return.
137     * @return mixed               Configured parameter value.
138     */
[468]139    public function getParam($param)
[1]140    {
[479]141        $app =& App::getInstance();
[136]142   
[478]143        if (array_key_exists($param, $this->_params)) {
[1]144            return $this->_params[$param];
145        } else {
[146]146            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
[1]147            return null;
148        }
149    }
[42]150
[1]151    /**
152     * Check whether any errors have been triggered.
153     *
154     * @return bool   True if any errors were found, false otherwise.
155     */
[468]156    public function anyErrors()
[1]157    {
158        return (sizeof($this->_errors) > 0);
159    }
160
161    /**
162     * Reset the error list.
163     */
[468]164    public function resetErrorList()
[1]165    {
166        $this->_errors = array();
167    }
[42]168
[1]169    /**
170     * Check one word.
171     *
172     * @access  public
173     * @param   string  $word
174     * @return  bool    True if word is correct.
175     * @author  Quinn Comendant <quinn@strangecode.com>
176     * @version 1.0
177     * @since   09 Jun 2005 18:23:51
178     */
[468]179    public function check($word)
[1]180    {
181        if (pspell_check($this->_pspell_handle, $word)) {
182            return true;
183        } else {
184            $this->_errors[] = $word;
185            return false;
186        }
187    }
[42]188
[1]189    /**
190     * Suggest the correct spelling for one misspelled word.
191     *
192     * @access  public
193     * @param   string  $word
194     * @return  array   Word suggestions.
195     * @author  Quinn Comendant <quinn@strangecode.com>
196     * @version 1.0
197     * @since   09 Jun 2005 18:23:51
198     */
[468]199    public function suggest($word)
[1]200    {
201        return pspell_suggest($this->_pspell_handle, $word);
202    }
[42]203
[1]204    /**
205     * Add a word to a personal list.
206     *
207     * @access  public
208     * @param   string  $word
209     * @return  array   Word suggestions.
210     * @author  Quinn Comendant <quinn@strangecode.com>
211     * @version 1.0
212     * @since   09 Jun 2005 18:23:51
213     */
[468]214    public function add($word)
[1]215    {
[479]216        $app =& App::getInstance();
[136]217
[1]218        if ($this->_use_personal_wordlist) {
[15]219            if (pspell_add_to_personal($this->_pspell_handle, $word)) {
[136]220                $app->logMsg(sprintf('Added "%s" to personal wordlist: %s', $word, $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
[42]221                return true;
222            } else {
[136]223                $app->logMsg(sprintf('Failed adding "%s" to personal wordlist: %s', $word, $this->getParam('personal_wordlist')), LOG_WARNING, __FILE__, __LINE__);
[42]224                return false;
225            }
[1]226        }
227    }
[42]228
[1]229    /**
230     * Save personal list to file.
231     *
232     * @access  public
233     * @param   string  $word
234     * @return  array   Word suggestions.
235     * @author  Quinn Comendant <quinn@strangecode.com>
236     * @version 1.0
237     * @since   09 Jun 2005 18:23:51
238     */
[468]239    public function save()
[1]240    {
[479]241        $app =& App::getInstance();
[136]242
[1]243        if ($this->_use_personal_wordlist) {
244            if (pspell_save_wordlist($this->_pspell_handle)) {
[136]245                $app->logMsg(sprintf('Saved personal wordlist: %s', $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
[1]246                return true;
247            } else {
[136]248                $app->logMsg(sprintf('Failed saving personal wordlist: %s', $this->getParam('personal_wordlist')), LOG_ERR, __FILE__, __LINE__);
[1]249                return false;
250            }
251        }
252    }
[42]253
[1]254    /**
[334]255     * Returns an array of suggested words for each misspelled word in the given text.
[1]256     * The first word of the returned array is the (possibly) misspelled word.
257     *
258     * @access  public
259     * @param   string  $string String to get suggestions for.
260     * @return  mixed   Array of suggested words or false if none.
261     * @author  Quinn Comendant <quinn@strangecode.com>
262     * @version 1.0
263     * @since   09 Jun 2005 21:29:49
264     */
[468]265    public function getStringSuggestions($string)
[1]266    {
267        $corrections = array();
[425]268        // Split words on punctuation except apostrophes
269        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
270        $words = preg_split("/((^\p{P}+)|(\p{P}*\s+\p{P}*)|[\p{Pd}-–—]+|(\+|(\p{P}+$))/", $string);
[1]271        // Remove non-word elements.
272        $words = preg_grep('/\w+/', $words);
273
274        if (is_array($words) && !empty($words)) {
275            foreach ($words as $i => $word) {
276                if (!$this->check($word)) {
277                    $corrections[$i] = $this->suggest($word);
278                    // Keep the original spelling as one of the suggestions.
279                    array_unshift($corrections[$i], $word);
280                    array_unique($corrections[$i]);
281                }
282            }
283        }
284        if (is_array($corrections) && !empty($corrections)) {
285            return $corrections;
286        } else {
287            return false;
288        }
289    }
[42]290
[1]291    /**
292     * Checks all words in a given string.
293     *
294     * @access  public
295     * @param   string  $string     String to check.
296     * @return  void
297     * @author  Quinn Comendant <quinn@strangecode.com>
298     * @version 1.0
299     * @since   09 Jun 2005 22:11:27
300     */
[468]301    public function checkString($string)
[1]302    {
303        $errors = array();
304        $words = preg_split('/([\W]+?)/', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
305        // Remove non-word elements.
306        $check_words = preg_grep('/\w+/', $words);
307
308        if (is_array($check_words) && !empty($check_words)) {
309            foreach ($check_words as $i => $word) {
310                if (!$this->check($word)) {
311                    $errors[] = $word;
312                }
313            }
314        }
315        if (empty($errors)) {
316            return true;
317        } else {
318            $this->_errors = $errors + $this->_errors;
319            return false;
320        }
321    }
[42]322
[1]323    /**
324     * Returns a given string with misspelled words highlighted.
325     *
326     * @access  public
327     * @param   string  $string     Text to highlight.
328     * @return  string  Highlighted text.
329     * @author  Quinn Comendant <quinn@strangecode.com>
330     * @version 1.0
331     * @since   09 Jun 2005 21:29:49
332     */
[468]333    public function getStringHighlighted($string, $show_footnote=false)
[1]334    {
335        $words = preg_split('/([\W]+?)/', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
336        $check_words = preg_grep('/\w+/', $words);
337        $cnt = 0;
338        if (is_array($check_words) && !empty($check_words)) {
339            foreach ($check_words as $i => $word) {
340                if (!$this->check($word)) {
341                    $footnote = $show_footnote ? '<sup>' . ++$cnt . '</sup>' : '';
342                    $words[$i] = $this->getParam('highlight_start') . $word . $this->getParam('highlight_end') . $footnote;
343                }
344            }
345        }
346        return join('', $words);
347    }
[42]348
[1]349    /**
[334]350     * Prints the HTML for correcting all misspellings found in the text of one $_FORM element.
[1]351     *
352     * @access  public
353     * @param   string  $form_name  Name of the form to check.
354     * @return  void
355     * @author  Quinn Comendant <quinn@strangecode.com>
356     * @version 1.0
357     * @since   09 Jun 2005 21:29:49
358     */
[468]359    public function printCorrectionForm($form_name)
[1]360    {
361        ?>
362        <input name="<?php echo $form_name ?>" type="hidden" value="<?php echo getFormData($form_name) ?>" />
363        <?php
[42]364
[1]365        $form_words = $this->getStringSuggestions(getFormData($form_name));
366        if (is_array($form_words) && !empty($form_words)) {
367            ?><ol><?php
368            foreach ($form_words as $i => $words) {
369                ?>
370                <li>
[270]371                <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]372                <?php $original_word = array_shift($words); ?>
373                <option value="<?php echo $original_word ?>">(<?php echo $original_word ?>)</option>
374                <?php
[42]375
[1]376                foreach ($words as $suggestion) {
377                    ?>
378                    <option value="<?php echo $suggestion ?>"><?php echo $suggestion ?></option>
379                    <?php
380                }
[42]381
[1]382                ?>
383                </select>
[176]384                <input type="text" name="spelling_corrections[<?php echo $form_name ?>][<?php echo $i ?>]" value="<?php echo $original_word ?>" class="sc-small" />
[1]385                <?php if ($this->_use_personal_wordlist) { ?>
386                <input name="save_to_personal_wordlist[]" type="checkbox" value="<?php echo $i ?>" /><?php echo _("Learn spelling") ?>
387                <?php } ?>
388                </li>
389                <?php
390            }
391            ?></ol><?php
392        }
393    }
[42]394
[1]395    /**
396     * Tests if any form spelling corrections have been submitted.
397     *
398     * @access  public
399     * @return  bool    True if form spelling has been checked.
400     * @author  Quinn Comendant <quinn@strangecode.com>
401     * @version 1.0
402     * @since   09 Jun 2005 23:15:35
403     */
[468]404    public function anyFormCorrections()
[1]405    {
406        return (false !== getFormData('spelling_suggestions', false)) || (false !== getFormData('spelling_corrections', false));
407    }
[42]408
[1]409    /**
410     * Replace the misspelled words in the text of a specified form with the corrections.
411     *
412     * @access  public
413     * @param   string  $form_name      Name of form to apply corrections to.
414     * @return  string  Corrected form text.
415     * @author  Quinn Comendant <quinn@strangecode.com>
416     * @version 1.0
417     * @since   09 Jun 2005 23:18:51
418     */
[468]419    public function applyFormCorrections($form_name)
[1]420    {
421        $form_words = preg_split('/([\W]+?)/', getFormData($form_name), -1, PREG_SPLIT_DELIM_CAPTURE);
422        $suggestions = getFormData('spelling_suggestions');
423        $corrections = getFormData('spelling_corrections');
424
425        $form_words = array_diff($corrections[$form_name], array('')) + $suggestions[$form_name] + $form_words;
426        ksort($form_words);
427
428        if ($this->_use_personal_wordlist) {
429            $save_to_personal_wordlist = getFormData('save_to_personal_wordlist');
430            if (is_array($save_to_personal_wordlist) && !empty($save_to_personal_wordlist)) {
431                foreach ($save_to_personal_wordlist as $cust) {
432                    $this->add($form_words[$cust]);
433                }
434            }
435            $this->save();
436        }
437
438        if (is_array($form_words) && !empty($form_words)) {
439            return join('', $form_words);
440        } else {
441            return getFormData($form_name);
442        }
443    }
444
445} // End.
446
Note: See TracBrowser for help on using the repository browser.