diff options
-rw-r--r-- | conf/dokuwiki.php | 2 | ||||
-rw-r--r-- | inc/aspell.php | 236 | ||||
-rw-r--r-- | inc/common.php | 2 | ||||
-rw-r--r-- | inc/html.php | 19 | ||||
-rw-r--r-- | inc/lang/de/lang.php | 6 | ||||
-rw-r--r-- | inc/lang/en/lang.php | 6 | ||||
-rw-r--r-- | inc/template.php | 7 | ||||
-rw-r--r-- | inc/utf8.php | 16 | ||||
-rw-r--r-- | lib/exe/ajax.php | 17 | ||||
-rw-r--r-- | lib/exe/spellcheck.php | 204 | ||||
-rw-r--r-- | lib/scripts/ajax.js | 10 | ||||
-rw-r--r-- | lib/scripts/spellcheck.js | 326 | ||||
-rw-r--r-- | lib/scripts/tw-sack.js | 2 | ||||
-rw-r--r-- | lib/tpl/default/design.css | 44 |
14 files changed, 882 insertions, 15 deletions
diff --git a/conf/dokuwiki.php b/conf/dokuwiki.php index c40728d0a..67b2d9936 100644 --- a/conf/dokuwiki.php +++ b/conf/dokuwiki.php @@ -69,6 +69,7 @@ $conf['locktime'] = 15*60; //maximum age for lockfiles (defaults t $conf['notify'] = ''; //send change info to this email (leave blank for nobody) $conf['mailfrom'] = ''; //use this email when sending mails $conf['gdlib'] = 2; //the GDlib version (0, 1 or 2) 2 tries to autodetect +$conf['spellchecker']= 0; //enable Spellchecker (needs PHP >= 4.3.0 and aspell installed) //Set target to use when creating links - leave empty for same window $conf['target']['wiki'] = ''; @@ -85,4 +86,3 @@ $conf['ftp']['user'] = 'user'; $conf['ftp']['pass'] = 'password'; $conf['ftp']['root'] = '/home/user/htdocs'; -?> diff --git a/inc/aspell.php b/inc/aspell.php new file mode 100644 index 000000000..ea8a7e428 --- /dev/null +++ b/inc/aspell.php @@ -0,0 +1,236 @@ +<?php +/** + * Aspell interface + * + * This library gives full access to aspell's pipe interface. Optionally it + * provides some of the functions from the pspell PHP extension by wrapping + * them to calls to the aspell binary. + * + * It can be simply dropped into code written for the pspell extension like + * the following + * + * if(!function_exists('pspell_suggest')){ + * define('PSPELL_COMP',1); + * require_once ("pspell_comp.php"); + * } + * + * Define the path to the aspell binary like this if needed: + * + * define('ASPELL_BIN','/path/to/aspell'); + * + * @author Andreas Gohr <andi@splitbrain.org> + * @todo Not all pspell functions are supported + * + */ + +// path to your aspell binary +if(!defined('ASPELL_BIN')) define('ASPELL_BIN','aspell'); + + +if(!defined('PSPELL_FAST')) define(PSPELL_FAST,1); # Fast mode (least number of suggestions) +if(!defined('PSPELL_NORMAL')) define(PSPELL_NORMAL,2); # Normal mode (more suggestions) +if(!defined('PSPELL_BAD_SPELLERS')) define(PSPELL_BAD_SPELLERS,3); # Slow mode (a lot of suggestions) +if(!defined('ASPELL_ULTRA')) define(ASPELL_ULTRA,4); # Ultra fast mode (not available in Pspell!) + +/** + * You can define PSPELL_COMP to use this class as drop in replacement + * for the pspell extension + */ +if(defined('PSPELL_COMP')){ + // spelling is not supported by aspell and ignored + function pspell_config_create($language, $spelling=null, $jargon=null, $encoding='iso8859-1'){ + return new Aspell($language, $jargon, $encoding); + } + + function pspell_config_mode(&$config, $mode){ + return $config->setMode($mode); + } + + function pspell_new_config(&$config){ + return $config; + } + + function pspell_check(&$dict,$word){ + return $dict->check($word); + } + + function pspell_suggest(&$dict, $word){ + return $dict->suggest($word); + } +} + +/** + * Class to interface aspell + * + * Needs PHP >= 4.3.0 + */ +class Aspell{ + var $language = null; + var $jargon = null; + var $encoding = 'iso8859-1'; + var $mode = PSPELL_NORMAL; + + var $args=''; + + /** + * Constructor. Works like pspell_config_create() + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function Aspell($language, $jargon=null, $encoding='iso8859-1'){ + $this->language = $language; + $this->jargon = $jargon; + $this->encoding = $encoding; + $this->_prepareArgs(); + } + + /** + * Set the spelling mode like pspell_config_mode() + * + * Mode can be PSPELL_FAST, PSPELL_NORMAL, PSPELL_BAD_SPELLER or ASPELL_ULTRA + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function setMode($mode){ + if(!in_array($mode,array(PSPELL_FAST,PSPELL_NORMAL,PSPELL_BAD_SPELLER,ASPELL_ULTRA))){ + $mode = PSPELL_NORMAL; + } + + $this->mode = $mode; + $this->_prepareArgs(); + return $mode; + } + + /** + * Prepares the needed arguments for the call to the aspell binary + * + * No need to call this directly + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function _prepareArgs(){ + $this->args = ''; + + if($this->language){ + $this->args .= ' --lang='.escapeshellarg($this->language); + }else{ + return false; // no lang no spell + } + + if($this->jargon){ + $this->args .= ' --jargon='.escapeshellarg($this->jargon); + } + + if($this->encoding){ + $this->args .= ' --encoding='.escapeshellarg($this->encoding); + } + + switch ($this->mode){ + case PSPELL_FAST: + $this->args .= ' --sug-mode=fast'; + break; + case PSPELL_BAD_SPELLERS: + $this->args .= ' --sug-mode=bad-spellers'; + break; + case ASPELL_ULTRA: + $this->args .= ' --sug-mode=ultra'; + break; + default: + $this->args .= ' --sug-mode=normal'; + } + + return true; + } + + /** + * Pipes a text to aspell + * + * This opens a bidirectional pipe to the aspell binary, writes + * the given text to STDIN and returns STDOUT and STDERR + * + * You have full access to aspell's pipe mode here - this means you need + * quote your lines yourself read the aspell manual for more info + * + * @author Andreas Gohr <andi@splitbrain.org> + * @link http://aspell.sf.net/man-html/Through-A-Pipe.html + */ + function runAspell($text,&$out,&$err){ + if(empty($text)) return true; + + //prepare file descriptors + $descspec = array( + 0 => array('pipe', 'r'), // stdin is a pipe that the child will read from + 1 => array('pipe', 'w'), // stdout is a pipe that the child will write to + 2 => array('pipe', 'w') // stderr is a file to write to + ); + + $process = proc_open(ASPELL_BIN.' -a'.$this->args, $descspec, $pipes); + if (is_resource($process)) { + //write to stdin + fwrite($pipes[0],$text); + fclose($pipes[0]); + + //read stdout + while (!feof($pipes[1])) { + $out .= fread($pipes[1], 8192); + } + fclose($pipes[1]); + + //read stderr + while (!feof($pipes[2])) { + $err .= fread($pipes[2], 8192); + } + fclose($pipes[2]); + + if(proc_close($process) != 0){ + //something went wrong + trigger_error("aspell returned an error: $err", E_USER_WARNING); + return null; + } + return true; + } + //opening failed + trigger_error("Could not run aspell '".ASPELL_BIN."'", E_USER_WARNING); + return false; + } + + /** + * Checks a single word for correctness + * + * @returns array of suggestions or true on correct spelling + * @author Andreas Gohr <andi@splitbrain.org> + */ + function suggest($word){ + if($this->runAspell("^$word",$out,$err)){ + //parse output + $lines = split("\n",$out); + foreach ($lines as $line){ + $line = trim($line); + if(empty($line)) continue; // empty line + if($line[0] == '@') continue; // comment + if($line[0] == '*') return true; // no mistakes made + if($line[0] == '#') return array(); // mistake but no suggestions + if($line[0] == '&'){ + $line = preg_replace('/&.*?: /','',$line); + return split(', ',$line); + } + } + } + return array(); + } + + /** + * Check if a word is mispelled like pspell_check + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + function check($word){ + if(is_array($this->suggest($word))){ + return false; + }else{ + return true; + } + } +} + +//Setup VIM: ex: et ts=4 enc=utf-8 : diff --git a/inc/common.php b/inc/common.php index 99cec10a9..80c866815 100644 --- a/inc/common.php +++ b/inc/common.php @@ -739,7 +739,7 @@ function filesize_h($size, $dec = 1){ } /** - * Run a few sanity checks + * Return DokuWikis version * * @author Andreas Gohr <andi@splitbrain.org> */ diff --git a/inc/html.php b/inc/html.php index d972c1abb..bd34bb609 100644 --- a/inc/html.php +++ b/inc/html.php @@ -833,6 +833,8 @@ function html_edit($text=null,$include='edit'){ //FIXME: include needed? $ro='readonly="readonly"'; } if(!$DATE) $DATE = $INFO['lastmod']; + + ?> <form name="editform" method="post" action="<?=script()?>" accept-charset="<?=$lang['encoding']?>" onsubmit="return svchk()"> <input type="hidden" name="id" value="<?=$ID?>" /> @@ -842,7 +844,7 @@ function html_edit($text=null,$include='edit'){ //FIXME: include needed? <input type="hidden" name="suffix" value="<?=formText($SUF)?>" /> <table style="width:99%"> <tr> - <td class="toolbar" colspan="3"> + <td class="toolbar" colspan="2"> <?if($wr){?> <script language="JavaScript" type="text/javascript"> <?/* sets changed to true when previewed */?> @@ -852,7 +854,7 @@ function html_edit($text=null,$include='edit'){ //FIXME: include needed? formatButton('italic.png','<?=$lang['qb_italic']?>',"\/\/","\/\/",'<?=$lang['qb_italic']?>','i'); formatButton('underline.png','<?=$lang['qb_underl']?>','__','__','<?=$lang['qb_underl']?>','u'); formatButton('code.png','<?=$lang['qb_code']?>','\'\'','\'\'','<?=$lang['qb_code']?>','c'); - formatButton('strike.png','<?=$lang['qb_strike']?>','<del>','</del>','<?=$lang['qb_strike']?>','d'); + formatButton('strike.png','<?=$lang['qb_strike']?>','<del>','<\/del>','<?=$lang['qb_strike']?>','d'); formatButton('fonth1.png','<?=$lang['qb_h1']?>','====== ',' ======\n','<?=$lang['qb_h1']?>','1'); formatButton('fonth2.png','<?=$lang['qb_h2']?>','===== ',' =====\n','<?=$lang['qb_h2']?>','2'); @@ -877,13 +879,20 @@ function html_edit($text=null,$include='edit'){ //FIXME: include needed? </script> <?}?> </td> + <td> +<!-- <span class="action" id="action"><a class="check_spelling" onClick="setObjToCheck('wikitext'); spellCheck();">Check Spelling</a></span> + <span class="status" id="status"></span> --> + <div id="spell_action"></div> + <div id="spell_suggest"></div> + </td> </tr> <tr> <td colspan="3"> + <div id="spell_result"></div> <textarea name="wikitext" id="wikitext" <?=$ro?> cols="80" rows="10" class="edit" onchange="textChanged = true;" onkeyup="summaryCheck();" tabindex="1"><?="\n".formText($text)?></textarea> </td> </tr> - <tr> + <tr id="wikieditbar"> <td> <?if($wr){?> <input class="button" type="submit" name="do" value="<?=$lang['btn_save']?>" accesskey="s" title="[ALT+S]" onclick="textChanged=false" onkeypress="textChanged=false" tabindex="3" /> @@ -902,6 +911,10 @@ function html_edit($text=null,$include='edit'){ //FIXME: include needed? showSizeCtl(); <?if($wr){?> init_locktimer(<?=$conf['locktime']-60?>,'<?=$lang['willexpire']?>'); + + //initialize spellchecker + ajax_spell.init('<?=$lang['spell_start']?>','<?=$lang['spell_stop']?>','<?=$lang['spell_wait']?>','<?=$lang['spell_noerr']?>','<?=$lang['spell_nosug']?>'); + document.editform.wikitext.focus(); <?}?> </script> diff --git a/inc/lang/de/lang.php b/inc/lang/de/lang.php index bc6ff749a..325e46044 100644 --- a/inc/lang/de/lang.php +++ b/inc/lang/de/lang.php @@ -130,4 +130,10 @@ $lang['acl_perm8'] = 'Hochladen'; $lang['acl_perm16'] = 'Entfernen'; $lang['acl_new'] = 'Eintrag hinzufügen'; +$lang['spell_start']= 'Rechtschreibung prüfen'; +$lang['spell_stop'] = 'Bearbeiten fortsetzen'; +$lang['spell_wait'] = 'Bitte warten...'; +$lang['spell_noerr']= 'Keine Fehler gefunden'; +$lang['spell_nosug']= 'Keine Vorschläge'; + //Setup VIM: ex: et ts=2 enc=utf-8 : diff --git a/inc/lang/en/lang.php b/inc/lang/en/lang.php index ba8736114..385e5a7e0 100644 --- a/inc/lang/en/lang.php +++ b/inc/lang/en/lang.php @@ -129,4 +129,10 @@ $lang['acl_perm8'] = 'Upload'; $lang['acl_perm16'] = 'Delete'; $lang['acl_new'] = 'Add new Entry'; +$lang['spell_start']= 'Check Spelling'; +$lang['spell_stop'] = 'Resume Editing'; +$lang['spell_wait'] = 'Please wait...'; +$lang['spell_noerr']= 'No Mistakes found'; +$lang['spell_nosug']= 'No Suggestions'; + //Setup VIM: ex: et ts=2 enc=utf-8 : diff --git a/inc/template.php b/inc/template.php index 43c224926..035bfbdc1 100644 --- a/inc/template.php +++ b/inc/template.php @@ -135,6 +135,7 @@ function tpl_metaheaders(){ global $INFO; global $ACT; global $lang; + global $conf; $it=2; // the usual stuff @@ -179,6 +180,12 @@ function tpl_metaheaders(){ ptln('<script language="JavaScript" type="text/javascript" src="'. DOKU_BASE.'lib/scripts/ajax.js"></script>',$it); + // load spellchecker script if wanted + if($conf['spellchecker'] && ($ACT=='edit' || $ACT=='preview')){ + ptln('<script language="JavaScript" type="text/javascript" src="'. + DOKU_BASE.'lib/scripts/spellcheck.js"></script>',$it); + } + //FIXME include some default CSS ? IE FIX? } diff --git a/inc/utf8.php b/inc/utf8.php index 0b90c8586..70b16f1a5 100644 --- a/inc/utf8.php +++ b/inc/utf8.php @@ -105,7 +105,7 @@ function utf8_check($Str) { * @see utf8_decode() */ function utf8_strlen($string){ - return strlen(utf8_decode($str)); + return strlen(utf8_decode($string)); } /** @@ -126,6 +126,20 @@ function utf8_substr($str,$start,$length=null){ } /** + * Unicode aware replacement for substr_replace() + * + * @author Andreas Gohr <andi@splitbrain.org> + * @see substr_replace() + */ +function utf8_substr_replace($string, $replacement, $start , $length=0 ){ + $ret = ''; + if($start>0) $ret .= utf8_substr($string, 0, $start); + $ret .= $replacement; + $ret .= utf8_substr($string, $start+$length); + return $ret; +} + +/** * Unicode aware replacement for explode * * @TODO support third limit arg diff --git a/lib/exe/ajax.php b/lib/exe/ajax.php index dfe0d2ceb..02911b858 100644 --- a/lib/exe/ajax.php +++ b/lib/exe/ajax.php @@ -20,7 +20,7 @@ require_once(DOKU_INC.'inc/auth.php'); //call the requested function $call = 'ajax_'.$_POST['call']; if(function_exists($call)){ - $call(); + $call(); }else{ print "The called function does not exist!"; } @@ -34,20 +34,21 @@ function ajax_qsearch(){ global $conf; global $lang; - $query = cleanID($_POST['q']); - if(empty($query)) return; + $query = cleanID($_POST['q']); + if(empty($query)) return; - $nsdir = str_replace(':','/',getNS($query)); - require_once(DOKU_INC.'inc/search.php'); - require_once(DOKU_INC.'inc/html.php'); + $nsdir = str_replace(':','/',getNS($query)); + require_once(DOKU_INC.'inc/search.php'); + require_once(DOKU_INC.'inc/html.php'); $data = array(); search($data,$conf['datadir'],'search_qsearch',array(query => $query),$nsdir); - if(!count($data)) return; + if(!count($data)) return; - print '<b>'.$lang['quickhits'].'</b>'; + print '<b>'.$lang['quickhits'].'</b>'; print html_buildlist($data,'qsearch','html_list_index'); } +//Setup VIM: ex: et ts=2 enc=utf-8 : ?> diff --git a/lib/exe/spellcheck.php b/lib/exe/spellcheck.php new file mode 100644 index 000000000..c6a883e32 --- /dev/null +++ b/lib/exe/spellcheck.php @@ -0,0 +1,204 @@ +<?php +/** + * DokuWiki Spellcheck AJAX backend + * + * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) + * @author Andreas Gohr <andi@splitbrain.org> + */ + +/** + * Licence info: This spellchecker is inspired by code by Garrison Locke available + * at http://www.broken-notebook.com/spell_checker/index.php (licensed under the Terms + * of an BSD license). The code in this file was nearly completly rewritten for DokuWiki + * and is licensed under GPL version 2 (See COPYING for details). + * + * Original Copyright notice follows: + * + * Copyright (c) 2005, Garrison Locke + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of the http://www.broken-notebook.com nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ + +//fix for Opera XMLHttpRequests +if(!count($_POST) && $HTTP_RAW_POST_DATA){ + parse_str($HTTP_RAW_POST_DATA, $_POST); +} + +if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); +require_once (DOKU_INC.'inc/init.php'); +require_once (DOKU_INC.'inc/utf8.php'); +require_once (DOKU_INC.'inc/aspell.php'); + +//create spell object +$spell = new Aspell($conf['lang'],'null','utf-8'); +$spell->setMode(PSPELL_FAST); + +//call the requested function +$call = 'spell_'.$_POST['call']; +if(function_exists($call)){ + $call(); +}else{ + print "The called function does not exist!"; +} + + +function spell_check() { + global $spell; + $string = $_POST['data']; + $misspell = false; + + // for streamlined line endings + $string = preg_replace("/(\015\012)|(\015)/","\012",$string); + $string = htmlspecialchars($string); + + // we need the text as array later + $data = explode("\n",$string); + + //prepare for aspell (add ^ to prevent commands) + $string = '^'.join("\n^",$data); + + // keep some words from being checked (use blanks to preserve the offset) FIXME doesn't work yet +/* $string = preg_replace('!<\?(code|del|file)( \+)?>!e','spellclean(\\1)',$string); */ +// $string = preg_replace('!()!e','spellclean(\\1)',$string); + + // run aspell in terse sgml mode + $spell->runAspell("!\n+sgml\n".$string,$out,$err); + + // go through the result + $lines = split("\n",$out); + $rcnt = count($lines)-1; // aspell result count + $lcnt = count($data)+1; // original line counter + + for($i=$rcnt; $i>=0; $i--){ + $line = trim($lines[$i]); + if($line[0] == '@') continue; // comment + if($line[0] == '*') continue; // no mistake in this word + if(empty($line)){ + // empty line -> new source line + $lcnt--; + continue; + } + if(preg_match('/^& ([^ ]+) (\d+) (\d+): (.*)/',$line,$match)){ + // match with suggestions + $word = $match[1]; + $off = $match[3]-1; + $sug = split(', ',$match[4]); + $len = utf8_strlen($word); + $misspell = true; + + + $data[$lcnt] = utf8_substr_replace($data[$lcnt], spell_formatword($word,$sug) , $off, $len); + continue; + } + if(preg_match('/^# ([^ ]+) (\d+)/',$line,$match)){ + // match without suggestions + $word = $match[1]; + $off = $match[2]-1; + $len = utf8_strlen($word); + $misspell = true; + + $data[$lcnt] = utf8_substr_replace($data[$lcnt], spell_formatword($word) , $off, $len); + continue; + } + } + + // the first char returns the spell info + if($misspell){ + $string = '1'.join('<br />',$data); + }else{ + $string = '0'.join('<br />',$data); + } + + //output + print $string; +} + +function spell_formatword($word,$suggestions=null){ + static $i = 1; + + if(is_array($suggestions)){ + //restrict to maximum of 7 elements + $suggestions = array_slice($suggestions,0,7); + $suggestions = array_map('htmlspecialchars',$suggestions); + $suggestions = array_map('addslashes',$suggestions); + $sug = ",'".join("','",$suggestions)."'"; //build javascript args + }else{ + $sug = ''; + } + + $link = '<a href="javascript:ajax_spell.suggest('.$i.$sug.')" '. + 'class="spell_error" id="spell_error'.$i.'">'.htmlspecialchars($word).'</a>'; + $i++; + return $link; +} + +function spell_resume(){ + $text = $_POST['data']; + + $text = preg_replace("/(\r\n|\n|\r)/", "", $text); + $text = preg_replace("=<br */?>=i", "\n", $text); + + // remove HTML tags + $text = strip_tags($text); + + // restore quoted special chars + $text = unhtmlspecialchars($text); + + // output + print $text; +} + +function spell_suggest(){ + $id = $_POST['id']; + $word = $_POST['word']; + + + print $id."\n".$word; +} + +/** + * Reverse htmlspecialchars + * + * @author <donwilson at gmail dot com> + * @author Andreas Gohr <andi@splitbrain.org> + */ +function unhtmlspecialchars($string, $quotstyle=ENT_COMPAT){ + $string = str_replace ( '&', '&', $string ); + $string = str_replace ( '<', '<', $string ); + $string = str_replace ( '>', '>', $string ); + + if($quotstyle != ENT_NOQUOTES){ + $string = str_replace ( '"', '\"', $string ); + } + if($quotstyle == ENT_QUOTES){ + $string = str_replace ( ''', '\'', $string ); + $string = str_replace ( ''', '\'', $string ); + } + + return $string; +} + +//Setup VIM: ex: et ts=2 enc=utf-8 : +?> diff --git a/lib/scripts/ajax.js b/lib/scripts/ajax.js index b3c32e950..0a4183463 100644 --- a/lib/scripts/ajax.js +++ b/lib/scripts/ajax.js @@ -1,3 +1,13 @@ +/** + * AJAX functions for the pagename quicksearch + * + * We're using a global object with self referencing methods + * here to make callbacks work + * + * @license GPL2 (http://www.gnu.org/licenses/gpl.html) + * @author Andreas Gohr <andi@splitbrain.org> + */ + //prepare class function ajax_qsearch_class(){ this.sack = null; diff --git a/lib/scripts/spellcheck.js b/lib/scripts/spellcheck.js new file mode 100644 index 000000000..5b7833601 --- /dev/null +++ b/lib/scripts/spellcheck.js @@ -0,0 +1,326 @@ +/** + * DokuWiki Spellcheck AJAX clientside script + * + * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) + * @author Andreas Gohr <andi@splitbrain.org> + */ + +/** + * Licence info: This spellchecker is inspired by code by Garrison Locke available + * at http://www.broken-notebook.com/spell_checker/index.php (licensed under the Terms + * of an BSD license). The code in this file was nearly completly rewritten for DokuWiki + * and is licensed under GPL version 2 (See COPYING for details). + * + * Original Copyright notice follows: + * + * Copyright (c) 2005, Garrison Locke + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of the http://www.broken-notebook.com nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ + +/** + * Get the X offset of the top left corner of the given object + * + * @author Garrison Locke <http://www.broken-notebook.com> + */ +function findPosX(object){ + var curleft = 0; + var obj = document.getElementById(object); + if (obj.offsetParent){ + while (obj.offsetParent){ + curleft += obj.offsetLeft; + obj = obj.offsetParent; + } + } + else if (obj.x){ + curleft += obj.x; + } + return curleft; +} //end findPosX function + +/** + * Get the Y offset of the top left corner of the given object + * + * @author Garrison Locke <http://www.broken-notebook.com> + */ +function findPosY(object){ + var curtop = 0; + var obj = document.getElementById(object); + if (obj.offsetParent){ + while (obj.offsetParent){ + curtop += obj.offsetTop; + obj = obj.offsetParent; + } + } + else if (obj.y){ + curtop += obj.y; + } + return curtop; +} //end findPosY function + + +/** + * AJAX Spellchecker Class + * + * Note to some function use a hardcoded instance named ajax_spell to make + * references to object members. Used Object-IDs are hardcoded in the init() + * method. + * + * @author Andreas Gohr <andi@splitbrain.org> + * @author Garrison Locke <http://www.broken-notebook.com> + */ +function ajax_spell_class(){ + this.inited = false; + this.handler = DOKU_BASE+'lib/exe/spellcheck.php'; + // to hold the page objects (initialized with init()) + this.textboxObj = null; + this.showboxObj = null; + this.suggestObj = null; + this.actionObj = null; + this.editbarObj = null; + // hold translations + this.txtStart = 'Check Spelling'; + this.txtStop = 'Resume Editing'; + this.txtRun = 'Checking...'; + this.txtNoErr = 'No Mistakes'; + this.txtNoSug = 'No Suggestions'; + + + /** + * Initializes everything + * + * Call after the page was setup. Hardcoded element IDs here. + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + this.init = function(txtStart,txtStop,txtRun,txtNoErr,txtNoSug){ + // don't run twice + if (this.inited) return; + this.inited = true; + + // check for AJAX availability + var ajax = new sack(); + if(ajax.failed) return; + + // get Elements + this.textboxObj = document.getElementById('wikitext'); + this.editbarObj = document.getElementById('wikieditbar'); + this.showboxObj = document.getElementById('spell_result'); + this.suggestObj = document.getElementById('spell_suggest'); + this.actionObj = document.getElementById('spell_action'); + + // set Translation Strings + this.txtStart = txtStart; + this.txtStop = txtStop; + this.txtRun = txtRun; + this.txtNoErr = txtNoErr; + this.txtNoSug = txtNoSug; + + // register click event + document.onclick = this.docClick; + + // register focus event + this.textboxObj.onfocus = this.setState; + + this.setState('start'); + } + + /** + * Eventhandler for click objects anywhere on the document + * + * Disables the suggestion box + * + * @author Andreas Gohr <andi@splitbrain.org> + * @author Garrison Locke <http://www.broken-notebook.com> + */ + this.docClick = function(e){ + // what was clicked? + try{ + target = window.event.srcElement; + }catch(ex){ + target = e.target; + } + + if (target.id != ajax_spell.suggestObj.id){ + ajax_spell.suggestObj.style.display = "none"; + } + } + + /** + * Changes the Spellchecker link according to the given mode + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + this.setState = function(state){ + switch (state){ + case 'stop': + ajax_spell.actionObj.innerHTML = + '<a class="spell_resume" href="javascript:ajax_spell.resume()">'+ajax_spell.txtStop+'</a>'; + break; + case 'noerr': + ajax_spell.actionObj.innerHTML = + '<span class="spell_noerr">'+ajax_spell.txtNoErr+'</span>'; + break; + case 'run': + ajax_spell.actionObj.innerHTML = + '<span class="spell_wait">'+ajax_spell.txtRun+'</span>'; + break; + default: + ajax_spell.actionObj.innerHTML = + '<a class="spell_start" href="javascript:ajax_spell.run()">'+ajax_spell.txtStart+'</a>'; + break; + } + } + + /** + * Replaces a word identified by id with its correction given in word + * + * @author Garrison Locke <http://www.broken-notebook.com> + */ + this.correct = function (id, word){ + var obj = document.getElementById('spell_error'+id); + obj.innerHTML = decodeURI(word); + obj.style.color = "#005500"; + this.suggestObj.style.display = "none"; + } + + // --- Callbacks --- + + /** + * Callback. Called after finishing spellcheck. + * Inside the callback 'this' is the SACK object!! + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + this.start = function(){ + var data = this.response; + var error = data.charAt(0); + data = data.substring(1); + if(error == '1'){ + ajax_spell.setState('stop'); + // replace textbox through div + ajax_spell.showboxObj.innerHTML = data; + ajax_spell.showboxObj.style.width = ajax_spell.textboxObj.style.width; + ajax_spell.showboxObj.style.height = ajax_spell.textboxObj.style.height; + ajax_spell.textboxObj.style.display = 'none'; + ajax_spell.editbarObj.style.visibility = 'hidden'; + ajax_spell.showboxObj.style.display = 'block'; + }else{ + ajax_spell.textboxObj.disabled = false; + ajax_spell.editbarObj.style.visibility = 'visible'; + ajax_spell.setState('noerr'); + } + } + + /** + * Callback. Gets called by resume() - switches back to edit mode + * Inside the callback 'this' is the SACK object!! + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + this.stop = function(){ + var data = this.response; + ajax_spell.setState('start'); + // replace div with textbox again + ajax_spell.textboxObj.value = data; + ajax_spell.textboxObj.disabled = false; + ajax_spell.showboxObj.style.display = 'none'; + ajax_spell.textboxObj.style.display = 'block'; + ajax_spell.editbarObj.style.visibility = 'visible'; + ajax_spell.showboxObj.innerHTML = ''; + } + + // --- Callers --- + + /** + * Starts the spellchecking by sending an AJAX request + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + this.run = function(){ + this.setState('run'); + this.textboxObj.disabled = true; + var ajax = new sack(this.handler); + ajax.encodeURIString = false; + ajax.onCompletion = this.start; + ajax.runAJAX('call=check&data='+encodeURIComponent(this.textboxObj.value)); + } + + /** + * Rewrites the HTML back to text again using an AJAX request + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + this.resume = function(){ + this.setState('run'); + var text = this.showboxObj.innerHTML; + if(text != ''){ + var ajax = new sack(this.handler); + ajax.encodeURIString = false; + ajax.onCompletion = this.stop; + ajax.runAJAX('call=resume&data='+encodeURIComponent(text)); + } + } + + /** + * Displays the suggestions for a misspelled word + * + * @author Andreas Gohr <andi@splitbrain.org> + * @author Garrison Locke <http://www.broken-notebook.com> + */ + this.suggest = function(){ + var args = this.suggest.arguments; + if(!args[0]) return; + var id = args[0]; + + // set position of the popup + this.suggestObj.style.display = "none"; + var x = findPosX('spell_error'+id); + var y = findPosY('spell_error'+id); + var scrollPos = this.showboxObj.scrollTop; + this.suggestObj.style.left = x+'px'; + this.suggestObj.style.top = (y+16-scrollPos)+'px'; + + // handle suggestions + var text = ''; + if(args.length == 1){ + text += this.txtNoSug; + }else{ + for(var i=1; i<args.length; i++){ + text += '<a href="javascript:ajax_spell.correct('+id+',\''+encodeURIComponent(args[i])+'\')">'; + text += args[i]; + text += '</a><br>'; + } + } + + this.suggestObj.innerHTML = text; + this.suggestObj.style.display = "block"; + } +} + +// create the global object +ajax_spell = new ajax_spell_class(); + +//Setup VIM: ex: et ts=2 enc=utf-8 : diff --git a/lib/scripts/tw-sack.js b/lib/scripts/tw-sack.js index 8a9d12746..18b63730a 100644 --- a/lib/scripts/tw-sack.js +++ b/lib/scripts/tw-sack.js @@ -47,7 +47,7 @@ function sack(file){ urlVars[1] = encodeURIComponent(urlVars[1]); varArray[i] = urlVars.join("="); } - return varArray.join('&'); + return varArray.join('&'); } this.runResponse = function(){ diff --git a/lib/tpl/default/design.css b/lib/tpl/default/design.css index e492dfec3..9eff3d0d5 100644 --- a/lib/tpl/default/design.css +++ b/lib/tpl/default/design.css @@ -640,3 +640,47 @@ div.ajax_qsearch { opacity: 0.9; display:none; } + +/* ---------- Spellchecking ------------- */ + +a.spell_error { + color: #ff0000; + text-decoration: underline; +} + +div#spell_suggest { + position: absolute; + left: 0; + top: 0; + display: none; + background-color: #fff; + padding: 2px; + border: 1px solid #000; + font-size:80%; +} + +div#spell_result { + display:none; + font-family:monospace; + border: 1px solid #8cacbb; + color: Black; + font-size:14px; + padding: 3px; + background-color: #f7f9fa; + overflow: auto; + + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ +} + +span.spell_noerr { + color: #009933; +} + +span.spell_wait { + color: #0066cc; +} + |