Search


Change Language


How to build an AJAX-ed spellchecker with Ruby On Rails
Tuesday, 20 March 2007
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>
spell.check('speling') #misspelling obviously
=> false
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.
 
< Prev   Next >
Life Insurance Credit Counseling Loans Buy Anything On eBay Credit Card
There are 220 free ajax scripts and 31 categories in our directory





Lost Password?
No account yet? Register
We have 9 guests online