1<?php
2/**
3 * Aspell interface
4 *
5 * This library gives full access to aspell's pipe interface. Optionally it
6 * provides some of the functions from the pspell PHP extension by wrapping
7 * them to calls to the aspell binary.
8 *
9 * It can be simply dropped into code written for the pspell extension like
10 * the following
11 *
12 * if(!function_exists('pspell_suggest')){
13 *   define('PSPELL_COMP',1);
14 *   require_once ("pspell_comp.php");
15 * }
16 *
17 * Define the path to the aspell binary like this if needed:
18 *
19 * define('ASPELL_BIN','/path/to/aspell');
20 *
21 * @author   Andreas Gohr <andi@splitbrain.org>
22 * @todo     Not all pspell functions are supported
23 *
24 */
25
26// path to your aspell binary
27if(!defined('ASPELL_BIN')) define('ASPELL_BIN','aspell');
28
29
30// different spelling modes supported by aspell
31if(!defined('PSPELL_FAST'))         define(PSPELL_FAST,1);         # Fast mode (least number of suggestions)
32if(!defined('PSPELL_NORMAL'))       define(PSPELL_NORMAL,2);       # Normal mode (more suggestions)
33if(!defined('PSPELL_BAD_SPELLERS')) define(PSPELL_BAD_SPELLERS,3); # Slow mode (a lot of suggestions)
34if(!defined('ASPELL_ULTRA'))        define(ASPELL_ULTRA,4);        # Ultra fast mode (not available in Pspell!)
35
36
37
38/**
39 * You can define PSPELL_COMP to use this class as drop in replacement
40 * for the pspell extension
41 */
42if(defined('PSPELL_COMP')){
43    // spelling is not supported by aspell and ignored
44    function pspell_config_create($language, $spelling=null, $jargon=null, $encoding='iso8859-1'){
45        return new Aspell($language, $jargon, $encoding);
46    }
47
48    function pspell_config_mode(&$config, $mode){
49        return $config->setMode($mode);
50    }
51
52    function pspell_new_config(&$config){
53        return $config;
54    }
55
56    function pspell_check(&$dict,$word){
57        return $dict->check($word);
58    }
59
60    function pspell_suggest(&$dict, $word){
61        return $dict->suggest($word);
62    }
63}
64
65/**
66 * Class to interface aspell
67 *
68 * Needs PHP >= 4.3.0
69 */
70class Aspell{
71    var $language = null;
72    var $jargon   = null;
73    var $personal = null;
74    var $encoding = 'iso8859-1';
75    var $mode     = PSPELL_NORMAL;
76    var $version  = 0;
77
78    var $args='';
79
80    /**
81     * Constructor. Works like pspell_config_create()
82     *
83     * @author   Andreas Gohr <andi@splitbrain.org>
84     */
85    function Aspell($language, $jargon=null, $encoding='iso8859-1'){
86        $this->language = $language;
87        $this->jargon   = $jargon;
88        $this->encoding = $encoding;
89    }
90
91    /**
92     * Set the spelling mode like pspell_config_mode()
93     *
94     * Mode can be PSPELL_FAST, PSPELL_NORMAL, PSPELL_BAD_SPELLER or ASPELL_ULTRA
95     *
96     * @author   Andreas Gohr <andi@splitbrain.org>
97     */
98    function setMode($mode){
99        if(!in_array($mode,array(PSPELL_FAST,PSPELL_NORMAL,PSPELL_BAD_SPELLER,ASPELL_ULTRA))){
100            $mode = PSPELL_NORMAL;
101        }
102
103        $this->mode = $mode;
104        return $mode;
105    }
106
107    /**
108     * Prepares the needed arguments for the call to the aspell binary
109     *
110     * No need to call this directly
111     *
112     * @author   Andreas Gohr <andi@splitbrain.org>
113     */
114    function _prepareArgs(){
115        $this->args = '';
116
117        if($this->language != null){
118            $this->args .= ' --lang='.escapeshellarg($this->language);
119        }else{
120            return false; // no lang no spell
121        }
122
123        if($this->jargon != null){
124            $this->args .= ' --jargon='.escapeshellarg($this->jargon);
125        }
126
127        if($this->personal != null){
128            $this->args .= ' --personal='.escapeshellarg($this->personal);
129        }
130
131        if($this->encoding != null){
132            $this->args .= ' --encoding='.escapeshellarg($this->encoding);
133        }
134
135        switch ($this->mode){
136            case PSPELL_FAST:
137                $this->args .= ' --sug-mode=fast';
138                break;
139            case PSPELL_BAD_SPELLERS:
140                $this->args .= ' --sug-mode=bad-spellers';
141                break;
142            case ASPELL_ULTRA:
143                $this->args .= ' --sug-mode=ultra';
144                break;
145            default:
146                $this->args .= ' --sug-mode=normal';
147        }
148
149        return true;
150    }
151
152
153    /**
154     * Pipes a text to aspell
155     *
156     * This opens a bidirectional pipe to the aspell binary, writes
157     * the given text to STDIN and returns STDOUT and STDERR
158     *
159     * You can give an array of special commands to be executed first
160     * as $specials parameter. Data lines are escaped automatically
161     *
162     * @author   Andreas Gohr <andi@splitbrain.org>
163     * @link     http://aspell.sf.net/man-html/Through-A-Pipe.html
164     */
165    function runAspell($text,&$out,&$err,$specials=null){
166        if(empty($text)) return true;
167        $terse = true;
168
169        // prepare arguments
170        $this->_prepareArgs();
171        $command = ASPELL_BIN.' -a'.$this->args;
172        $stdin   = '';
173
174        // prepare specials
175        if(is_array($specials)){
176            foreach($specials as $s){
177                if ($s == '!') $terse = false;
178                $stdin .= "$s\n";
179            }
180        }
181
182        // prepare text
183        $stdin .= "^".str_replace("\n", "\n^",$text);
184
185        // run aspell through the pipe
186        $rc = $this->execPipe($command,$stdin,$out,$err);
187        if(is_null($rc)){
188            $err = "Could not run Aspell '".ASPELL_BIN."'";
189            return false;
190        }
191
192        // Aspell has a bug that can't be autodetected because both versions
193        // might produce the same output but under different conditions. So
194        // we check Aspells version number here to divide broken and working
195        // versions of Aspell.
196        $tmp = array();
197        preg_match('/^\@.*Aspell (\d+)\.(\d+).(\d+)/',$out,$tmp);
198        $this->version = $tmp[1]*100 + $tmp[2]*10 + $tmp[3];
199
200        if ($this->version <= 603)  // version 0.60.3
201            $r = $terse ? "\n*\n\$1" : "\n\$1"; // replacement for broken Aspell
202        else
203            $r = $terse ? "\n*\n" : "\n";    // replacement for good Aspell
204
205        // lines starting with a '?' are no realy misspelled words and some
206        // Aspell versions doesn't produce usable output anyway so we filter
207        // them out here.
208        $out = preg_replace('/\n\? [^\n\&\*]*([\n]?)/',$r, $out);
209
210        if ($err){
211            //something went wrong
212            $err = "Aspell returned an error(".ASPELL_BIN." exitcode: $rc ):\n".$err;
213            return false;
214        }
215        return true;
216    }
217
218
219    /**
220     * Runs the given command with the given input on STDIN
221     *
222     * STDOUT and STDERR are written to the given vars, the command's
223     * exit code is returned. If the pip couldn't be opened null is returned
224     *
225     * @author <richard at 2006 dot atterer dot net>
226     * @link http://www.php.net/manual/en/function.proc-open.php#64116
227     */
228    function execPipe($command,$stdin,&$stdout,&$stderr){
229        $descriptorSpec = array(0 => array("pipe", "r"),
230                                1 => array('pipe', 'w'),
231                                2 => array('pipe', 'w'));
232        $process = proc_open($command, $descriptorSpec, $pipes);
233        if(!$process) return null;
234
235        $txOff = 0;
236        $txLen = strlen($stdin);
237        $stdoutDone = false;
238        $stderrDone = false;
239
240        stream_set_blocking($pipes[0], 0); // Make stdin/stdout/stderr non-blocking
241        stream_set_blocking($pipes[1], 0);
242        stream_set_blocking($pipes[2], 0);
243
244        if ($txLen == 0) fclose($pipes[0]);
245        while (true) {
246            $rx = array(); // The program's stdout/stderr
247            if (!$stdoutDone) $rx[] = $pipes[1];
248            if (!$stderrDone) $rx[] = $pipes[2];
249            $tx = array(); // The program's stdin
250            if ($txOff < $txLen) $tx[] = $pipes[0];
251            stream_select($rx, $tx, $ex = NULL, NULL, NULL); // Block til r/w possible
252
253            if (!empty($tx)) {
254                $txRet = fwrite($pipes[0], substr($stdin, $txOff, 8192));
255                if ($txRet !== false) $txOff += $txRet;
256                if ($txOff >= $txLen) fclose($pipes[0]);
257            }
258
259            foreach ($rx as $r) {
260                if ($r == $pipes[1]) {
261                    $stdout .= fread($pipes[1], 8192);
262                    if (feof($pipes[1])) {
263                        fclose($pipes[1]);
264                        $stdoutDone = true;
265                    }
266                } else if ($r == $pipes[2]) {
267                    $stderr .= fread($pipes[2], 8192);
268                    if (feof($pipes[2])) {
269                        fclose($pipes[2]);
270                        $stderrDone = true;
271                    }
272                }
273            }
274            if (!is_resource($process)) break;
275            if ($txOff >= $txLen && $stdoutDone && $stderrDone) break;
276        }
277        return proc_close($process);
278    }
279
280
281
282
283    /**
284     * Checks a single word for correctness
285     *
286     * @returns  array of suggestions or true on correct spelling
287     * @author   Andreas Gohr <andi@splitbrain.org>
288     */
289    function suggest($word){
290        if($this->runAspell("^$word",$out,$err)){
291            //parse output
292            $lines = split("\n",$out);
293            foreach ($lines as $line){
294                $line = trim($line);
295                if(empty($line))    continue;       // empty line
296                if($line[0] == '@') continue;       // comment
297                if($line[0] == '*') return true;    // no mistakes made
298                if($line[0] == '#') return array(); // mistake but no suggestions
299                if($line[0] == '&'){
300                    $line = preg_replace('/&.*?: /','',$line);
301                    return split(', ',$line);
302                }
303            }
304        }
305        return array();
306    }
307
308    /**
309     * Check if a word is mispelled like pspell_check
310     *
311     * @author   Andreas Gohr <andi@splitbrain.org>
312     */
313    function check($word){
314        if(is_array($this->suggest($word))){
315            return false;
316        }else{
317            return true;
318        }
319    }
320}
321
322//Setup VIM: ex: et ts=4 enc=utf-8 :
323