summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandi <andi@splitbrain.org>2005-06-07 21:44:56 +0200
committerandi <andi@splitbrain.org>2005-06-07 21:44:56 +0200
commitdc57ef04af4d12bb589c98e7457a22c44a091815 (patch)
tree8364d81819ec7745ce4e05c149256c40704f19da
parent50d915fed681509e84e625a5bfd4e3602dfe98d2 (diff)
downloadrpg-dc57ef04af4d12bb589c98e7457a22c44a091815.tar.gz
rpg-dc57ef04af4d12bb589c98e7457a22c44a091815.tar.bz2
AJAX spellchecker #29
This is nearly a complete rewrite of the gmail like AJAX spellchecker from http://www.broken-notebook.com/spell_checker/index.php Here are the differences and features * seemless integrated into DokuWiki * no need for the pspell extension * needs GNU aspell installed (not sure about the version I guess 0.60+ for UTF8) * needs PHP 4.3.0+ * uses SACK for AJAX * gets errors and suggestions in one transfer So far only tested in Firefox. It should work in IE, Safari and Opera 8, too. Please test and report back. darcs-hash:20050607194456-9977f-f699144d1fd28359742b2ce0f28c839a1f4cefbb.gz
-rw-r--r--conf/dokuwiki.php2
-rw-r--r--inc/aspell.php236
-rw-r--r--inc/common.php2
-rw-r--r--inc/html.php19
-rw-r--r--inc/lang/de/lang.php6
-rw-r--r--inc/lang/en/lang.php6
-rw-r--r--inc/template.php7
-rw-r--r--inc/utf8.php16
-rw-r--r--lib/exe/ajax.php17
-rw-r--r--lib/exe/spellcheck.php204
-rw-r--r--lib/scripts/ajax.js10
-rw-r--r--lib/scripts/spellcheck.js326
-rw-r--r--lib/scripts/tw-sack.js2
-rw-r--r--lib/tpl/default/design.css44
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 ( '&amp;', '&', $string );
+ $string = str_replace ( '&lt;', '<', $string );
+ $string = str_replace ( '&gt;', '>', $string );
+
+ if($quotstyle != ENT_NOQUOTES){
+ $string = str_replace ( '&quot;', '\"', $string );
+ }
+ if($quotstyle == ENT_QUOTES){
+ $string = str_replace ( '&#39;', '\'', $string );
+ $string = str_replace ( '&#039;', '\'', $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;
+}
+