How to build an AJAX-ed spellchecker with Ruby On Rails
If you are a web developer and you have not been shipwrecked on a desolate island for the last year, you have heard about Rails. With a good chance you have seen database driven web applications sketched together in an instance. And all this using a language almost unknown a few years ago. Ain’t just wonderful how the World changes on you in an instance?
I’m not going to write here about RoR’s beautiful object relational mapping layer called ActiveRecord. There are lot of tutorials showing you that already. Instead I’m going to use a non ActiveRecord back-end to build a spellchecker. OK I’m exaggerating the spellchecker is already built we will only add a simple AJAX based front-end to the best spellchecker I’ve seen.
For interacting with Aspell we have two choices. One option would be the command line, keeping an aspell process in the background and feeding it text via pipes. This is how Emacs and other programs do it. Second option, and the one I’m going to use it here, is calling the native functions implemented in the Aspell library.
Luckily a ruby binding to Aspell called raspell is already available. For now it doesn’t come in a gem format but expect this to change soon. Till then I’ll just install the extension using the old way, extracting the downloaded source and then:
$ ruby extconf.rb
$ make
$ make install
To reassure ourselves that everything installed fine lets drop to an interactive ruby session and play some with a spellchecker object:
irb(main):001:0> require 'raspell'
=> true
irb(main):002:0> spell = Aspell.new
=> #< Aspell:0xb7d275bc >
irb(main):003:0> spell.check('speling') #misspelling obviously
=> false
irb(main):004:0> spell.suggest('speling')
=> ["spelling", "spieling", "sapling", ...
So it works! Now generate a rails application with a controller to do the spelling, lets just call them demo and SpellController:
$rails demo
$cd demo
$script/generate controller spell
Next
we would like to collect some text from the user. With rails this means
adding a view, to render form’s HTML code, and an method to our SpellController enabling the user to initiate the action.
<!-- file app/views/spell/edit_text.rhtml -->
<p>Edit your text for misspelings!</p>
<%= form_tag :action => 'check_spelling' -%>
<%= text_area_tag( 'body',
@body.nil? ? "" : @body,
:size=>"40x15") %>
<br /><br />
<%= submit_tag "Check spelling" %>
<%= end_form_tag %>
#file app/controllers/spell_controller.rb
class SpellController < ApplicationController
def edit_text
@body = ""
end
end
OK so we got text, now we need to actually check spelling and decorate misspelled words. Here follows an action-view pair doing that and a bit more:
#file app/controllers/spell_controller.rb
def check_spelling
@aspell = Aspell.new
if params[:body]
session[:body] = params[:body].split($/)
#don't worry about this for now ...
session[:replacements] = { }
end
end
<!-- file app/views/spell/check_spelling.rhtml -->
<%= form_tag :action => 'edit_text' %>
<div class="text_body">
<%=
count = 0
@aspell.correct_lines(session[:body]) do |misspelled|
count = count + 1
render(:partial => "misspelled"
:object => misspelled
:locals => {:cnt => count})
end
-%>
</div>
<br /><br />
<%= submit_tag "Resume editing", :name => 'done' %>
<%= end_form_tag %>
Seems that there is too much logic in the view. This doesn’t
look too well. But wait there is something more than simple RHTML going
on here: Aspell#correct_lines will loop trough text
invoking supplied block for all misspelled words. These will be
replaced by string returned from the block: in our case string from
_misspelled.rhtml partial. That is strange use of partials, wouldn’t
you say?
You might ask why does it need to be so complicated? Well parsing natural text is not an easy task and Aspell already does it too well for me to re-implement it again. This way I don’t have to worry about punctuation and other tokens filtered by Aspell.
We would like to give the user a drop-down selection with correct spellings for each misspelled word. Something like done by Google Mail in their compose window. This means retrieving a list of suggestions via AJAX, and placing them in an absolute CSS positioned block element right bellow misspelled word.
<!-- file app/views/spell/_misspelled.rhtml -->
<%
suggest_div = "suggest__#{cnt}"
this_span = "id__#{cnt}"
%>
<span id='<%=this_span%>'>
<%=
link_to_remote(
misspelled
{ :complete => "showSuggestions('#{this_span}','#{suggest_div}')",
:update => suggest_div,
:condition =>"isInVisible('#{suggest_div}')",
:url => {
:action => 'suggest',
:id => misspelled,
:word_count => "#{cnt}"}}
:class => 'misspelled',
:title => 'Click for suggested spellings') -%>
</span>
<ul style="display:none" class="suggestions" id='<%=suggest_div%>'></ul>
Suggestions will appear in suggest_div, of course if they are not already shown. Whenever the user clicks a suggestion link a second AJAX call is needed to update this_span, with chosen spelling alternative. This is why we need a span wrapping the HTML anchor.
Code to show suggestions looks like:
#file app/controllers/spell_controller.rb
def suggest
@aspell = Aspell.new
render( :partial => 'suggest_item',
:collection => @aspell.suggest(params[:id]),
:locals => {:word_count => params[:word_count]})
end
and the view partial:
<!-- file app/views/spell/_suggest_item.rhtml -->
<li class="suggestion">
<%=
link_to_remote( suggest_item,
:complete => "new Element.remove('suggest__#{word_count}')",
:update => "id__#{word_count}",
:url => {
:action => 'replace',
:id => word_count,
:with => suggest_item}) -%>
<li>
Due to the way how Aspell filters text, we are not going to make the actual replacement on the fly. Rather we remember it in the session that a replacement took place for the misspelling with given id.
#file app/controllers/spell_controller.rb
def replace
ndx = params[:id].to_i
session[:replacements][ndx] = params[:with]
render :text => params[:with]
end
Modifications will be replayed for real when the user resumes
editing. Remember how we kept a count for each misspelling in the block
of Aspell#correct_lines? This count has double use. First it ensures
that each AJAX update target has an unique ID. And secondly we use it
to remember corrections that are replayed in the final version of edit_text.
#file app/controllers/spell_controller.rb
def edit_text
#apply corrections
if session[:body] && session[:replacements] && (session[:replacements].size > 0)
aspell = Aspell.new
count = 0
@body = aspell.correct_lines(session[:body]) do |misspelled|
count = count + 1
session[:replacements][count]
end
session[:replacements] = { }
session[:body] = @body
else
@body = session[:body];
end
@body.join($/) if @body
end
Apart from some Javascript code required to compute actual position where suggestion drop-down box should appear, that is pretty much it. You can play with a working demo thanks to my company Primalgrasp.
Comments
-
Wow; this is great, thank you! I did find a slight typo in the text area tag code.
:size=>”40×15” %>)
should be
:size=>”40×15”) %>
If anyone could help me out with a problem I’m having, I’d be truly grateful. I implemented the spellchecker as described here but when I click on one of the highlighted misspellings, nothing happens. Basically, the suggest function doesn’t run. The function runs if I comment out this line in the “misspelled” partial:
:condition =>”isInVisible(’#{suggest_div}’)”,
In this case, I can print to the log all the spelling suggestions but I simply can’t figure out what is preventing them from rendering on the page like they do on Dee’s demo on Primalgrasp. I’d really appreciate any insight that you can share that could lead me to discover what I’m doing wrong. Thanks for taking the time to help out a new Ruby developer! -
Thank you typo corrected. As for your problem: you’ll need to have a silly Javascript function available on the client side. I’ve called this isInvisble(), but you will probably want to rewrite it, as it is wrong in a zillion ways.
You might want to learn about rjs templates. This example was written about a year ago and it’s long complicated and horridly old fashioned ;-)p
