fixed: Server side spellchecker doesn't work

This commit is contained in:
Graham Knop 2008-08-01 20:38:20 +00:00
parent 26cc8ad9df
commit 2b8d9a7b48
9 changed files with 559 additions and 154 deletions

View file

@ -18,6 +18,7 @@
- fixed: DEmote in toolbar menu has PROmote url
- fixed: EMS purge now functions correctly
- improve behavior of preload.perl for custom lib dirs not ending in lib
- fixed: Server side spellchecker doesn't work
7.5.18
- fixed: Collateral Image Manager broken in Firefox 3

View file

@ -20,9 +20,6 @@ use WebGUI::Form;
use WebGUI::Utility;
use WebGUI::International;
use JSON;
BEGIN {
eval { require Text::Aspell }; # Optional
}
our @ISA = qw(WebGUI::Asset);
@ -494,7 +491,9 @@ sub getRichEditor {
);
foreach my $button (@toolbarButtons) {
if ($button eq "spellchecker" && $self->session->config->get('availableDictionaries')) {
push(@plugins,"spellchecker");
push(@plugins,"-wgspellchecker");
$loadPlugins{wgspellchecker} = $self->session->url->extras("tinymce-webgui/plugins/wgspellchecker/editor_plugin.js");
$config{spellchecker_rpc_url} = $self->session->url->gateway('', "op=spellCheck");
$config{spellchecker_languages} =
join(',', map { ($_->{default} ? '+' : '').$_->{name}.'='.$_->{id} } @{$self->session->config->get('availableDictionaries')});
}

View file

@ -11,14 +11,21 @@ package WebGUI::Operation::SpellCheck;
#-------------------------------------------------------------------
use strict;
use Encode;
# Optional, but if unavailable, spell checking will have no effect.
eval 'use Text::Aspell';
use WebGUI::Utility;
use File::Path qw(mkpath);
# Optional, but if unavailable, spell checking will have no effect.
my $spellerAvailable;
BEGIN {
eval {
require Text::Aspell;
};
$spellerAvailable = 1
unless $@;
};
=head1 NAME
Package WebGUI::Operation::Spellcheck
Package WebGUI::Operation::SpellCheck
=head1 DESCRIPTION
@ -28,7 +35,7 @@ Operation for server side spellchecking functions.
#-------------------------------------------------------------------
=head2 _getSpeller ( session )
=head2 _getSpeller ( session , language )
Returns an instanciated Text::Aspell object.
@ -36,99 +43,145 @@ Returns an instanciated Text::Aspell object.
An instanciated session object.
=head3 language
The language code to use for spell checking.
=cut
sub _getSpeller {
my ($baseDir, $userDir, $homeDir);
my $session = shift;
return undef unless Text::Aspell->can('new');
my $speller = Text::Aspell->new;
my $session = shift;
my $lang = shift;
die "Server side spellcheck not available\n"
unless $spellerAvailable;
# Get language
my $speller = Text::Aspell->new;
die "Language not available in server side spellcheck"
unless (isIn($lang, map {m/^.*?:([^:]*):.*?$/} $speller->list_dictionaries));
# Get language
my $lang = $session->form->process('lang');
return undef unless (isIn($lang, map {m/^.*?:([^:]*):.*?$/} $speller->list_dictionaries));
# User homedir
my $homeDir = $session->config->get('uploadsPath').'/dictionaries/';
# User homedir
my $userId = $session->user->userId;
my $userId = $session->user->userId;
if (length($userId) < 22) {
$homeDir .= "oldIds/$userId";
}
else {
$userId =~ m/^(.{2})(.{2})/;
$homeDir .= "$1/$2/$userId";
}
mkpath($homeDir) unless (-e $homeDir);
$baseDir = $session->config->get('uploadsPath').'/dictionaries/';
if (length($userId) < 22) {
$userDir = 'oldIds/'.$userId;
mkdir($baseDir.$userDir) unless (-e $baseDir.$userDir);
} else {
$userDir = $userId;
$userDir =~ s/^(.{2})(.{2})*$/$1\/$2\/$userId/;
mkdir($baseDir.$1) unless (-e $baseDir.$1);
mkdir($baseDir.$1.'/'.$2) unless (-e $baseDir.$1.'/'.$2);
}
$homeDir = $baseDir.$userDir;
mkdir($homeDir) unless (-e $homeDir);
# Set speller options.
$speller->set_option('home-dir', $homeDir);
$speller->set_option('lang', $lang);
return $speller;
# Set speller options.
$speller->set_option('home-dir', $homeDir);
$speller->set_option('lang', $lang);
return $speller;
}
#-------------------------------------------------------------------
=head2 _processOutput ( session, words, [ id, [ command ] ] )
=head2 addWord ( $session, $language, $word )
Processes the wordlist and generates an XML string that the TinyMCE spellchecker
plugin can grok.
Adds a word sent by the tinymce spellchecker plugin to the personal dictionary
of the the current user.
=head3 session
=head3 $session
The instanciated session object.
The instanciated session object
=head3 words
=head3 $language
An arrayref containing the words that you want to send back to the spellchecker
plugin.
The dictionary language to use.
=head3 id
=head3 $word
The id that the tinyMCE spellchecker plugin assigined to this specific action.
If not specified the value of the formparam 'id' will be sent.
=head3 command
The spellchecker plugin command that has been issued. If omitted the value of
formparam 'cmd' will be used.
The word to add to the dictionary.
=cut
sub _processOutput {
my $session = shift;
my $words = shift || [];
my $id = shift || $session->form->process('id');
my $command = shift || $session->form->process('cmd');
$session->http->setMimeType('text/xml; charset=utf-8');
my $output = '<?xml version="1.0" encoding="utf-8" ?>'."\n";
sub addWord {
my $session = shift;
my $language = shift;
my $word = shift;
die "You must be logged in to add words to your dictionary.\n:"
if ($session->user->userId eq '1');
my $speller = _getSpeller($session, $language);
$speller->add_to_personal($word);
$speller->save_all_word_lists;
return 1;
}
if (scalar(@$words) == 0) {
$output .= '<res id="'.$id.'" cmd="'.$command.'"></res>';
}
else {
$output .= '<res id="'.$id.'" cmd="'.$command.'">'.encode_utf8(join(" ", @$words)).'</res>';
}
#-------------------------------------------------------------------
return $output;
=head2 checkWords ( $session, $language, \@words )
Check the spelling on a list of words and returns a list of misspelled words as an array reference
=head3 $session
The instanciated session object
=head3 $language
The dictionary language to use.
=head3 \@word
The words to check the spelling of as an array reference.
=cut
sub checkWords {
my $session = shift;
my $language = shift;
my $words = shift;
my $speller = _getSpeller($session, $language);
my @result;
foreach my $word (@$words) {
unless ($speller->check($word)) {
push(@result, $word);
}
}
return \@result;
}
#-------------------------------------------------------------------
=head2 getSuggestions ( $session, $language, $word )
Returns a list of suggested words for a misspelled word sent by the
tinyMCE spellchecker as an array reference.
=head3 $session
The instanciated session object.
=head3 $language
The dictionary language to use.
=head3 $word
The misspelled word to get suggestions for.
=cut
sub getSuggestions {
my $session = shift;
my $language = shift;
my $word = shift;
my $speller = _getSpeller($session, $language);
my @result = $speller->suggest($word);
return \@result;
}
#-------------------------------------------------------------------
=head2 www_spellCheck ( session )
Fetches the the text to be checked as sent by the tinyMCE spellchecker and
returns a list of erroneous words in the correct XML format.
Fetches the JSON data sent by the TinyMCE spell checker and dispatches
to the correct sub to handle each request type. Encodes the result
as a JSON string to be sent back to the client.
=head3 session
@ -137,87 +190,37 @@ The instanciated session object.
=cut
sub www_spellCheck {
my $session = shift;
my (@result, $output);
my $session = shift;
# JSON data is sent directly as POST data, read it into a scalar then decode
my $data = '';
while ($session->request->read(my $buffer, 1024)) {
$data .= $buffer;
}
my $params = JSON->new->utf8->decode($data);
my $speller = _getSpeller($session);
return _processOutput($session) unless (defined($speller));
# Set speller options?
# Get form params
my $check = $session->form->process('check');
my $command = $session->form->process('cmd');
my $language = $session->form->process('lang');
my $mode = $session->form->process('mode');
my $id = $session->form->process('id');
# Check it!
my @words = split(/\s/, $check);
foreach my $word (@words) {
unless ($speller->check($word)) {
push(@result, $word);
}
}
return _processOutput($session, \@result);
}
#-------------------------------------------------------------------
=head2 www_suggestWords ( session )
Returns a list of suggested words in the correct XML format for a misspelled
word sent by the tinyMCE spellchecker.
=head3 session
The instanciated session object.
=cut
sub www_suggestWords {
my $session = shift;
my $speller = _getSpeller($session);
return _processOutput($session) unless (defined($speller));
my $check = $session->form->process('check');
my @result = $speller->suggest($check);
return _processOutput($session, \@result);
}
#-------------------------------------------------------------------
=head2 www_addWordToDictionary ( session )
Adds a word sent by the tinymce spellchecker plugin to the personal dictionary
of the the current user.
=head3 session
The instanciated session object
=cut
sub www_addWordToDictionary {
my $session = shift;
# Visitors do not have a personal dictionary
return _processOutput($session, ['You must be logged in to add words to your dictionary.']) if ($session->user->userId eq '1');
my $speller = _getSpeller($session);
return _processOutput($session) unless (defined($speller));
my $check = $session->form->process('check');
if ($check) {
$speller->add_to_personal($check);
$speller->save_all_word_lists;
}
return _processOutput($session);
my $result;
# dispatch to different subs based on the 'method' in the JSON data
my %dispatch = (
checkWords => \&checkWords,
getSuggestions => \&getSuggestions,
addWord => \&addWord,
);
if (exists $dispatch{$params->{method}}) {
eval {
# get results from sub and build result data
$result = { result => $dispatch{$params->{method}}->($session, @{ $params->{params} }) };
};
if ($@) {
$result = {error => {errstr => $@}};
}
}
else {
$result = {error => {errstr => "Invalid request"}};
}
# add request id and send to client as JSON blob
$result->{id} = $params->{id};
$session->http->setMimeType("text/plain; charset=utf-8");
return JSON->new->encode($result);
}
1;

View file

@ -0,0 +1,48 @@
diff --git a/editor_plugin_src.js b/editor_plugin_src.js
index 4e9ba99..a96e310 100644
--- a/editor_plugin_src.js
+++ b/editor_plugin_src.js
@@ -7,6 +7,7 @@
(function() {
var JSONRequest = tinymce.util.JSONRequest, each = tinymce.each, DOM = tinymce.DOM;
+ tinymce.PluginManager.requireLangPack('wgspellchecker');
tinymce.create('tinymce.plugins.SpellcheckerPlugin', {
getInfo : function() {
@@ -269,6 +270,16 @@
}
});
+ m.add({
+ title : 'spellchecker.add_word',
+ onclick : function() {
+ t._sendRPC('addWord', [t.selectedLang, dom.decode(e.target.innerHTML)], function(r) {
+ t._removeWords(dom.decode(e.target.innerHTML));
+ t._checkDone();
+ });
+ }
+ });
+
m.update();
});
@@ -333,5 +344,5 @@
});
// Register plugin
- tinymce.PluginManager.add('spellchecker', tinymce.plugins.SpellcheckerPlugin);
-})();
\ No newline at end of file
+ tinymce.PluginManager.add('wgspellchecker', tinymce.plugins.SpellcheckerPlugin);
+})();
diff --git a/dev/null b/langs/en.js
new file mode 100644
index 0000000..602b23c
--- /dev/null
+++ b/langs/en.js
@@ -0,0 +1,4 @@
+tinyMCE.addI18n('en.spellchecker',{
+ add_word : 'Add word to dictionary'
+});
+

View file

@ -0,0 +1 @@
.mceItemHiddenSpellWord {background:url(../img/wline.gif) repeat-x bottom left; cursor:default;}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,348 @@
/**
* $Id: editor_plugin_src.js 425 2007-11-21 15:17:39Z spocke $
*
* @author Moxiecode
* @copyright Copyright © 2004-2008, Moxiecode Systems AB, All rights reserved.
*/
(function() {
var JSONRequest = tinymce.util.JSONRequest, each = tinymce.each, DOM = tinymce.DOM;
tinymce.PluginManager.requireLangPack('wgspellchecker');
tinymce.create('tinymce.plugins.SpellcheckerPlugin', {
getInfo : function() {
return {
longname : 'Spellchecker',
author : 'Moxiecode Systems AB',
authorurl : 'http://tinymce.moxiecode.com',
infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/spellchecker',
version : tinymce.majorVersion + "." + tinymce.minorVersion
};
},
init : function(ed, url) {
var t = this, cm;
t.url = url;
t.editor = ed;
// Register commands
ed.addCommand('mceSpellCheck', function() {
if (!t.active) {
ed.setProgressState(1);
t._sendRPC('checkWords', [t.selectedLang, t._getWords()], function(r) {
if (r.length > 0) {
t.active = 1;
t._markWords(r);
ed.setProgressState(0);
ed.nodeChanged();
} else {
ed.setProgressState(0);
ed.windowManager.alert('spellchecker.no_mpell');
}
});
} else
t._done();
});
ed.onInit.add(function() {
ed.dom.loadCSS(url + '/css/content.css');
});
ed.onClick.add(t._showMenu, t);
ed.onContextMenu.add(t._showMenu, t);
ed.onBeforeGetContent.add(function() {
if (t.active)
t._removeWords();
});
ed.onNodeChange.add(function(ed, cm) {
cm.setActive('spellchecker', t.active);
});
ed.onSetContent.add(function() {
t._done();
});
ed.onBeforeGetContent.add(function() {
t._done();
});
ed.onBeforeExecCommand.add(function(ed, cmd) {
if (cmd == 'mceFullScreen')
t._done();
});
// Find selected language
t.languages = {};
each(ed.getParam('spellchecker_languages', '+English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr,German=de,Italian=it,Polish=pl,Portuguese=pt,Spanish=es,Swedish=sv', 'hash'), function(v, k) {
if (k.indexOf('+') === 0) {
k = k.substring(1);
t.selectedLang = v;
}
t.languages[k] = v;
});
},
createControl : function(n, cm) {
var t = this, c, ed = t.editor;
if (n == 'spellchecker') {
c = cm.createSplitButton(n, {title : 'spellchecker.desc', cmd : 'mceSpellCheck', scope : t});
c.onRenderMenu.add(function(c, m) {
m.add({title : 'spellchecker.langs', 'class' : 'mceMenuItemTitle'}).setDisabled(1);
each(t.languages, function(v, k) {
var o = {icon : 1}, mi;
o.onclick = function() {
mi.setSelected(1);
t.selectedItem.setSelected(0);
t.selectedItem = mi;
t.selectedLang = v;
};
o.title = k;
mi = m.add(o);
mi.setSelected(v == t.selectedLang);
if (v == t.selectedLang)
t.selectedItem = mi;
})
});
return c;
}
},
// Internal functions
_walk : function(n, f) {
var d = this.editor.getDoc(), w;
if (d.createTreeWalker) {
w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false);
while ((n = w.nextNode()) != null)
f.call(this, n);
} else
tinymce.walk(n, f, 'childNodes');
},
_getSeparators : function() {
var re = '', i, str = this.editor.getParam('spellchecker_word_separator_chars', '\\s!"#$%&()*+,-./:;<=>?@[\]^_{|}§©«®±¶·¸»¼½¾¿×÷¤\u201d\u201c');
// Build word separator regexp
for (i=0; i<str.length; i++)
re += '\\' + str.charAt(i);
return re;
},
_getWords : function() {
var ed = this.editor, wl = [], tx = '', lo = {};
// Get area text
this._walk(ed.getBody(), function(n) {
if (n.nodeType == 3)
tx += n.nodeValue + ' ';
});
// Split words by separator
tx = tx.replace(new RegExp('([0-9]|[' + this._getSeparators() + '])', 'g'), ' ');
tx = tinymce.trim(tx.replace(/(\s+)/g, ' '));
// Build word array and remove duplicates
each(tx.split(' '), function(v) {
if (!lo[v]) {
wl.push(v);
lo[v] = 1;
}
});
return wl;
},
_removeWords : function(w) {
var ed = this.editor, dom = ed.dom, se = ed.selection, b = se.getBookmark();
each(dom.select('span').reverse(), function(n) {
if (n && (dom.hasClass(n, 'mceItemHiddenSpellWord') || dom.hasClass(n, 'mceItemHidden'))) {
if (!w || dom.decode(n.innerHTML) == w)
dom.remove(n, 1);
}
});
se.moveToBookmark(b);
},
_markWords : function(wl) {
var r1, r2, r3, r4, r5, w = '', ed = this.editor, re = this._getSeparators(), dom = ed.dom, nl = [];
var se = ed.selection, b = se.getBookmark();
each(wl, function(v) {
w += (w ? '|' : '') + v;
});
r1 = new RegExp('([' + re + '])(' + w + ')([' + re + '])', 'g');
r2 = new RegExp('^(' + w + ')', 'g');
r3 = new RegExp('(' + w + ')([' + re + ']?)$', 'g');
r4 = new RegExp('^(' + w + ')([' + re + ']?)$', 'g');
r5 = new RegExp('(' + w + ')([' + re + '])', 'g');
// Collect all text nodes
this._walk(this.editor.getBody(), function(n) {
if (n.nodeType == 3) {
nl.push(n);
}
});
// Wrap incorrect words in spans
each(nl, function(n) {
var v;
if (n.nodeType == 3) {
v = n.nodeValue;
if (r1.test(v) || r2.test(v) || r3.test(v) || r4.test(v)) {
v = dom.encode(v);
v = v.replace(r5, '<span class="mceItemHiddenSpellWord">$1</span>$2');
v = v.replace(r3, '<span class="mceItemHiddenSpellWord">$1</span>$2');
dom.replace(dom.create('span', {'class' : 'mceItemHidden'}, v), n);
}
}
});
se.moveToBookmark(b);
},
_showMenu : function(ed, e) {
var t = this, ed = t.editor, m = t._menu, p1, dom = ed.dom, vp = dom.getViewPort(ed.getWin());
if (!m) {
p1 = DOM.getPos(ed.getContentAreaContainer());
//p2 = DOM.getPos(ed.getContainer());
m = ed.controlManager.createDropMenu('spellcheckermenu', {
offset_x : p1.x,
offset_y : p1.y,
'class' : 'noIcons'
});
t._menu = m;
}
if (dom.hasClass(e.target, 'mceItemHiddenSpellWord')) {
m.removeAll();
m.add({title : 'spellchecker.wait', 'class' : 'mceMenuItemTitle'}).setDisabled(1);
t._sendRPC('getSuggestions', [t.selectedLang, dom.decode(e.target.innerHTML)], function(r) {
m.removeAll();
if (r.length > 0) {
m.add({title : 'spellchecker.sug', 'class' : 'mceMenuItemTitle'}).setDisabled(1);
each(r, function(v) {
m.add({title : v, onclick : function() {
dom.replace(ed.getDoc().createTextNode(v), e.target);
t._checkDone();
}});
});
m.addSeparator();
} else
m.add({title : 'spellchecker.no_sug', 'class' : 'mceMenuItemTitle'}).setDisabled(1);
m.add({
title : 'spellchecker.ignore_word',
onclick : function() {
dom.remove(e.target, 1);
t._checkDone();
}
});
m.add({
title : 'spellchecker.ignore_words',
onclick : function() {
t._removeWords(dom.decode(e.target.innerHTML));
t._checkDone();
}
});
m.add({
title : 'spellchecker.add_word',
onclick : function() {
t._sendRPC('addWord', [t.selectedLang, dom.decode(e.target.innerHTML)], function(r) {
t._removeWords(dom.decode(e.target.innerHTML));
t._checkDone();
});
}
});
m.update();
});
ed.selection.select(e.target);
p1 = dom.getPos(e.target);
m.showMenu(p1.x, p1.y + e.target.offsetHeight - vp.y);
return tinymce.dom.Event.cancel(e);
} else
m.hideMenu();
},
_checkDone : function() {
var t = this, ed = t.editor, dom = ed.dom, o;
each(dom.select('span'), function(n) {
if (n && dom.hasClass(n, 'mceItemHiddenSpellWord')) {
o = true;
return false;
}
});
if (!o)
t._done();
},
_done : function() {
var t = this, la = t.active;
if (t.active) {
t.active = 0;
t._removeWords();
if (t._menu)
t._menu.hideMenu();
if (la)
t.editor.nodeChanged();
}
},
_sendRPC : function(m, p, cb) {
var t = this, url = t.editor.getParam("spellchecker_rpc_url", "{backend}");
if (url == '{backend}') {
t.editor.setProgressState(0);
alert('Please specify: spellchecker_rpc_url');
return;
}
JSONRequest.sendRPC({
url : url,
method : m,
params : p,
success : cb,
error : function(e, x) {
t.editor.setProgressState(0);
t.editor.windowManager.alert(e.errstr || ('Error response: ' + x.responseText));
}
});
}
});
// Register plugin
tinymce.PluginManager.add('wgspellchecker', tinymce.plugins.SpellcheckerPlugin);
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 B

View file

@ -0,0 +1,4 @@
tinyMCE.addI18n('en.spellchecker',{
add_word : 'Add word to dictionary'
});