File Coverage

blib/lib/Text/SpellChecker.pm
Criterion Covered Total %
statement 25 25 100.0
branch 3 6 50.0
condition n/a
subroutine 6 6 100.0
pod n/a
total 34 37 91.8


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Text::SpellChecker - OO interface for spell-checking a block of text
4              
5             =head1 SYNOPSIS
6              
7             use Text::SpellChecker;
8             ($Text::SpellChecker::pre_hl_word,
9             $Text::SpellChecker::post_hl_word) = (qw([ ]));
10              
11             my $checker = Text::SpellChecker->new(text => "Foor score and seven yeers ago");
12              
13             while (my $word = $checker->next_word) {
14             print $checker->highlighted_text,
15             "\n",
16             "$word : ",
17             (join "\t", @{$checker->suggestions}),
18             "\nChoose a new word : ";
19             chomp (my $new_word = );
20             $checker->replace(new_word => $new_word) if $new_word;
21             }
22              
23             print "New text : ".$checker->text."\n";
24              
25             --or--
26              
27             use CGI;
28             use Text::SpellChecker;
29             my $q = new CGI;
30             print $q->header,
31             $q->start_html,
32             $q->start_form(-method=>'POST',-action=>$ENV{SCRIPT_NAME});
33              
34             my $checker = Text::SpellChecker->new(
35             text => "Foor score and seven yeers ago",
36             from_frozen => $q->param('frozen') # will be false the first time.
37             );
38              
39             $checker->replace(new_word => $q->param('replacement'))
40             if $q->param('replace');
41              
42             if (my $word = $checker->next_word) {
43             print $q->p($checker->highlighted_text),
44             $q->br,
45             qq|Next word : "$word"|,
46             $q->br,
47             $q->submit(-name=>'replace',-value=>'replace with:'),
48             $q->popup_menu(-name=>'replacement',-values=>$checker->suggestions),
49             $q->submit(-name=>'skip');
50             } else {
51             print "Done. New text : ".$checker->text;
52             }
53              
54             print $q->hidden(-name => 'frozen',
55             -value => $checker->serialize,
56             -override => 1),
57             $q->end_form,
58             $q->end_html;
59              
60              
61             =head1 DESCRIPTION
62              
63             This module is a thin layer above either Text::Aspell or Text::Hunspell (preferring
64             the latter if available), and allows one to spellcheck a body of text.
65              
66             Whereas Text::(Hu|A)spell deals with words, Text::Spellchecker deals with blocks of text.
67             For instance, we provide methods for iterating through the text, serializing the object (thus
68             remembering where we left off), and highlighting the current misspelled word within
69             the text.
70              
71             =head1 METHODS
72              
73             =over 4
74              
75             =item $checker = Text::SpellChecker->new(text => $text, from_frozen => $serialized_data, lang => $lang, options => $options)
76              
77             Send either the text or a serialized object to the constructor.
78             Optionally, the language of the text can also be passed.
79             If no language is passed, $ENV{LANG} will be used, if it is set.
80             If it is not set, the default language will be "en_US".
81              
82             $options are checker-specific options (see below).
83              
84             =item $checker = new_from_frozen($serialized_data)
85              
86             This is provided separately, so that it may be
87             overridden for alternative serialization techniques.
88              
89             =item $str=$checker->serialize
90              
91             Represent the object in its current state.
92              
93             =item $checker->reset
94              
95             Reset the checker to the beginning of the text, and clear the list of ignored words.
96              
97             =item $word = $checker->next_word
98              
99             Returns the next misspelled word.
100              
101             =item $checker->current_word
102              
103             Returns the most recently returned word.
104              
105             =item $checker->replace(new_word => $word)
106              
107             Replace the current word with $word.
108              
109             =item $checker->ignore_all
110              
111             Ignore all subsequent occurences of the current word.
112              
113             =item $checker->replace_all(new_word => $new_word)
114              
115             Replace all subsequent occurences of the current word with a new word.
116              
117             =item $checker->suggestions
118              
119             Returns a reference to a list of alternatives to the
120             current word in a scalar context, or the list directly
121             in a list context.
122              
123             =item $checker->text
124              
125             Returns the current text (with corrections that have been
126             applied).
127              
128             =item $checker->highlighted_text
129              
130             Returns the text, but with the current word surrounded by $Text::SpellChecker::pre_hl_word and
131             $Text::SpellChecker::post_hl_word.
132              
133             =item $checker->set_options
134              
135             Set checker-specific options. Currently only aspell supports setting options, e.g.
136              
137             $checker->set_options(aspell => { "extra-dicts" => "nl" } );
138              
139             =back
140              
141             =head1 CONFIGURATION OPTIONS
142              
143             =over
144              
145             =item $Text::SpellChecker::pre_hl_word
146              
147             Set this to control the highlighting of a misspelled word.
148              
149             =item $Text::SpellChecker::post_hl_word
150              
151             Set this to control the highlighting of a misspelled word.
152              
153             =item $Text::SpellCheckerDictionaryPath{Hunspell}
154              
155             Set this to the hunspell dictionary path. By default /usr/share/hunspell.
156              
157             This directory should have $lang.dic and $lang.aff files.
158            
159             =back
160              
161             =head1 LICENSE
162              
163             This library is free software; you can redistribute it and/or modify
164             it under the same terms as Perl itself.
165              
166             =head1 TODO
167              
168             Add word to custom dictionary
169              
170             =head1 SEE ALSO
171              
172             Text::Aspell, Text::Hunspell
173              
174             =head1 AUTHOR
175              
176             Brian Duggan
177              
178             =cut
179              
180             package Text::SpellChecker;
181 2     2   30522 use Carp;
  2         4  
  2         189  
182 2     2   4914 use Storable qw(freeze thaw);
  2         5595  
  2         130  
183 2     2   954 use MIME::Base64;
  2         999  
  2         105  
184 2     2   10 use warnings;
  2         2  
  2         53  
185 2     2   8 use strict;
  2         1  
  2         326  
186              
187             our $VERSION = '0.14';
188              
189             our $pre_hl_word = qq||;
190             our $post_hl_word = "";
191             our %SpellersAvailable;
192             BEGIN {
193             %SpellersAvailable = (
194 2 50       3 Aspell => do { eval{require Text::Aspell}; $@ ? 0 : 1},
  2         311  
  2         12  
195 2 50   2   4 Hunspell => do { eval{require Text::Hunspell}; $@ ? 0 : 1},
  2         2  
  2         261  
  2         11  
196             );
197 2 50       6 unless (grep { $_ } values %SpellersAvailable) {
  4         15  
198 2         89 die "Could not load Text::Aspell or Text::Hunspell. At least one must be installed";
199             };
200             }
201             our %DictionaryPath = (
202             Hunspell => q[/usr/share/hunspell]
203             );
204              
205             #
206             # new
207             #
208             # parameters :
209             # text : the text we're checking
210             # from_frozen : serialized class data to use instead of using text
211             #
212             sub new {
213             my ($class,%args) = @_;
214             return $class->new_from_frozen($args{from_frozen}) if $args{from_frozen};
215             bless {
216             text => $args{text},
217             ignore_list => {}, # keys of this hash are words to be ignored
218             ( lang => $args{lang} ) x !!$args{lang},
219             ( options => $args{options} ) x !!$args{options},
220             }, $class;
221             }
222              
223             sub set_options {
224             my ($self, %opts) = @_;
225             $self->{options} = \%opts;
226             }
227              
228             sub reset {
229             my $self = shift;
230             $self->{position} = undef;
231             $self->{ignore_list} = {};
232             }
233              
234             # Ignore all remaining occurences of the current word.
235              
236             sub ignore_all {
237             my $self = shift;
238             my $word = $self->current_word or croak "Can't ignore all : no current word";
239             $self->{ignore_list}{$word} = 1;
240             }
241              
242             # Replace all remaining occurences with the given word
243              
244             sub replace_all {
245             my ($self,%args) = @_;
246             my $new_word = $args{new_word} or croak "no replacement given";
247             my $current = $self->current_word;
248             $self->replace(new_word => $new_word);
249             my $saved_position = $self->{position};
250             while (my $next = $self->next_word) {
251             next unless $next eq $current;
252             $self->replace(new_word => $new_word);
253             }
254             $self->{position} = $saved_position;
255             }
256              
257             #
258             # new_from_frozen
259             #
260             # Alternative handy constructor using serialized object.
261             #
262             sub new_from_frozen {
263             my $class = shift;
264             my $frozen = shift;
265             my $self = thaw(decode_base64($frozen)) or croak "Couldn't unthaw $frozen";
266             unless (ref $self =~ /Spellchecker/i) {
267             bless $self, $class;
268             }
269             return $self;
270             }
271              
272             #
273             # next_word
274             #
275             # Get the next misspelled word.
276             # Returns false if there are no more.
277             #
278             sub next_word {
279             my $self = shift;
280             pos $self->{text} = $self->{position};
281             my $word;
282             my $sp = $self->_hunspell || $self->_aspell || die "Could not make a speller with Text::Hunspell or Text::Aspell.";
283             while ($self->{text} =~ m/\b(\p{L}+(?:'\p{L}+)?)/g) {
284             $word = $1;
285             next if $self->{ignore_list}{$word};
286             last if !$sp->check($word);
287             }
288             unless ($self->{position} = pos($self->{text})) {
289             $self->{current_word} = undef;
290             return undef;
291             }
292             $self->{suggestions} = [ $sp->suggest($word) ];
293             $self->{current_word} = $word;
294             return $word;
295             }
296              
297             #
298             # Private method returning a Text::Aspell object
299             #
300             sub _aspell {
301             my $self = shift;
302             return unless $SpellersAvailable{Aspell};
303              
304             unless ( $self->{aspell} ) {
305             $self->{aspell} = Text::Aspell->new;
306             $self->{aspell}->set_option( lang => $self->{lang} )
307             if $self->{lang};
308             if (my $opts = $self->{options}{aspell}) {
309             $self->{aspell}->set_option( $_ => $opts->{$_} ) for keys %$opts
310             }
311             }
312              
313             return $self->{aspell};
314             }
315              
316             sub _hunspell {
317             my $self = shift;
318             return unless $SpellersAvailable{Hunspell};
319             unless ( -d $DictionaryPath{Hunspell} ){
320             warn "Could not find hunspell dictionary directory $DictionaryPath{Hunspell}.";
321             return;
322             }
323             my $env_lang;
324             ($env_lang) = $ENV{LANG} =~ /^([^\.]*)/ if $ENV{LANG};
325             my $lang = $self->{lang} || $env_lang || "en_US";
326             my $dic = sprintf("%s/%s.dic", $DictionaryPath{Hunspell}, $lang );
327             my $aff = sprintf("%s/%s.aff", $DictionaryPath{Hunspell}, $lang );
328             -e $dic or do {
329             warn "Could not find $dic";
330             return;
331             };
332             -e $aff or do {
333             warn "could not find $aff";
334             return;
335             };
336              
337             unless ( $self->{hunspell} ) {
338             $self->{hunspell} = Text::Hunspell->new($aff,$dic);
339             }
340              
341             return $self->{hunspell};
342             }
343              
344             #
345             # replace - replace the current word with a new one.
346             #
347             # parameters :
348             # new_word - the replacement for the current word
349             #
350             sub replace {
351             my ($self,%args) = @_;
352             my $new_word = $args{new_word} or croak "no replacement given";
353             my $word = $self->current_word or croak "can't replace with $new_word : no current word";
354             $self->{position} -= length($word); # back up : we'll recheck this word, but that's okay.
355             substr($self->{text},$self->{position},length($word)) = $new_word;
356             }
357              
358             #
359             # highlighted_text
360             #
361             # Get the text with the current misspelled word highlighted.
362             #
363             sub highlighted_text {
364             my $self = shift;
365             my $word = $self->current_word;
366             return $self->{text} unless ($word and $self->{position});
367             my $text = $self->{text};
368             substr($text,$self->{position} - length($word),length($word)) = "$pre_hl_word$word$post_hl_word";
369             return $text;
370             }
371              
372             #
373             # Some accessors
374             #
375             sub text { return $_[0]->{text}; }
376             sub suggestions {
377             return unless $_[0]->{suggestions};
378             return wantarray
379             ? @{$_[0]->{suggestions}}
380             : $_[0]->{suggestions}
381             ;
382             }
383             sub current_word { return $_[0]->{current_word}; }
384              
385             #
386             # Handy serialization method.
387             #
388             sub serialize {
389             my $self = shift;
390              
391             # remove mention of Aspell object, if any
392             my %copy = %$self;
393             delete $copy{aspell};
394             delete $copy{hunspell};
395              
396             return encode_base64 freeze \%copy;
397             }
398              
399             1;
400