File Coverage

blib/lib/DDG/Block/Words.pm
Criterion Covered Total %
statement 53 55 96.3
branch 18 20 90.0
condition n/a
subroutine 10 12 83.3
pod 2 3 66.6
total 83 90 92.2


line stmt bran cond sub pod time code
1             package DDG::Block::Words;
2             our $AUTHORITY = 'cpan:DDG';
3             # ABSTRACT: Block implementation to handle words based plugins
4             $DDG::Block::Words::VERSION = '1016';
5 9     9   2377 use Moo;
  9         3497  
  9         42  
6 9     9   1786 use Carp;
  9         10  
  9         10628  
7             with qw( DDG::Block );
8              
9             sub BUILD {
10 13     13 0 63 my ( $self ) = @_;
11 13         13 for (@{$self->plugin_objs}) {
  13         39  
12 28         111 my $triggers = $_->[0];
13 28         28 my $plugin = $_->[1];
14 28         25 for (@{$triggers}) {
  28         34  
15 28         23 my $trigger = $_;
16 28 50       72 croak "trigger must be a hash on ".(ref $plugin) unless ref $trigger eq 'HASH';
17 28 100       58 if (defined $trigger->{start}) {
18 7         9 $self->_set_start_word_plugin($_,$plugin) for (@{$trigger->{start}});
  7         17  
19             }
20 28 100       81 if (defined $trigger->{end}) {
21 5         6 $self->_set_end_word_plugin($_,$plugin) for (@{$trigger->{end}});
  5         15  
22             }
23 28 100       67 if (defined $trigger->{startend}) {
24 3         3 $self->_set_start_word_plugin($_,$plugin) for (@{$trigger->{startend}});
  3         10  
25 3         15 $self->_set_end_word_plugin($_,$plugin) for (@{$trigger->{startend}});
  3         7  
26             }
27 28 100       62 if (defined $trigger->{any}) {
28 23         24 $self->_set_any_word_plugin($_,$plugin) for (@{$trigger->{any}});
  23         56  
29             }
30             }
31             }
32             }
33              
34              
35 11     11   25 sub _set_start_word_plugin { shift->_set_beginword_word_plugin('start',@_) }
36 30     30   97 sub _set_any_word_plugin { shift->_set_beginword_word_plugin('any',@_) }
37 9     9   23 sub _set_end_word_plugin { shift->_set_endword_word_plugin('end',@_) }
38              
39             # Grab trigger word (or FIRST word from trigger phrase)
40             # to use as hash key for `start` and `any` trigger hashes
41             sub _set_endword_word_plugin {
42 9     9   11 my ( $self, $type, $word, $plugin ) = @_;
43 9         45 my @words = split(/\s+/,$word);
44 9         12 $word = join(' ',@words);
45 9         16 $self->_set_word_plugin($type,pop @words,$word,$plugin);
46             }
47              
48             # Grab trigger word (or LAST word from trigger phrase)
49             # to use as hash key for `end` trigger hash
50             sub _set_beginword_word_plugin {
51 41     41   51 my ( $self, $type, $word, $plugin ) = @_;
52 41         116 my @words = split(/\s+/,$word);
53 41         57 $word = join(' ',@words);
54 41         65 $self->_set_word_plugin($type,shift @words,$word,$plugin);
55             }
56              
57             sub _set_word_plugin {
58 50     50   48 my ( $self, $type, $key, $word, $plugin ) = @_;
59 50         91 my @split_word = split(/\s+/,$word);
60 50         42 my $word_count = scalar @split_word;
61 50 100       686 $self->_words_plugins->{$type}->{$key} = {} unless defined $self->_words_plugins->{$type}->{$key};
62 50 100       719 if ($word_count eq 1) {
63 32 100       364 $self->_words_plugins->{$type}->{$key}->{1} = [] unless defined $self->_words_plugins->{$type}->{$key}->{$word_count};
64 32         543 push @{$self->_words_plugins->{$type}->{$key}->{1}}, $plugin;
  32         384  
65             } else {
66 18 100       212 $self->_words_plugins->{$type}->{$key}->{$word_count} = {} unless defined $self->_words_plugins->{$type}->{$key}->{$word_count};
67 18 50       507 $self->_words_plugins->{$type}->{$key}->{$word_count}->{$word} = [] unless defined $self->_words_plugins->{$type}->{$key}->{$word_count}->{$word};
68 18         330 push @{$self->_words_plugins->{$type}->{$key}->{$word_count}->{$word}}, $plugin;
  18         231  
69             }
70             }
71              
72              
73             has _words_plugins => (
74             # like HashRef[HashRef[DDG::Block::Plugin]]',
75             is => 'ro',
76             lazy => 1,
77             builder => 1,
78             );
79 0     0 1 0 sub words_plugins { shift->_words_plugins }
80              
81             sub _build__words_plugins {{
82 10     10   232 start => {},
83             end => {},
84             any => {},
85             }}
86              
87              
88             sub request {
89             my ( $self, $request ) = @_;
90             my @results;
91             #
92             # Mapping positions of keywords in the request
93             # to a flat array which access stepwise.
94             #
95             # @poses is an array of the positions inside
96             # the triggers hash.
97             #
98             ################################################
99             my %triggers = %{$request->triggers};
100             my $max = scalar keys %triggers;
101             my @poses = sort { $a <=> $b } keys %triggers;
102             $self->trace( "Trigger word positions: ", @poses );
103             for my $cnt (0..$max-1) {
104             #
105             # Split into a flat array so we can easily
106             # determine if the query is starting, ending
107             # or still in the beginning, this is very essential
108             # for the following steps.
109             #
110             my $start = $cnt == 0 ? 1 : 0;
111             my $end = $cnt == $max-1 ? 1 : 0;
112              
113             # Iterate over each word in the query, checking if any of
114             # the IA's have this specific word as (part of) a `start`,
115             # `end` or `any` trigger.
116             for my $word (@{$request->triggers->{$poses[$cnt]}}) {
117             $self->trace( "Testing word:", "'".$word."'" );
118              
119             my @hits = ();
120             {
121              
122             # Flag that tells us if there could be more words
123             # after the matched word.
124             #
125             # Used for `any` and `start` triggers. Not for `end`
126             # triggers because they look in-front of the word.
127             my $begin = 0;
128              
129             # A hash containing all triggers related to the current word
130             #
131             # The keys inside the hitstruct define the word count for the
132             # trigger word/phrase. This is used to determine if the query
133             # is long enough to have a match.
134             # ie. a 2 word query can't match a 3 word trigger phrase
135             my $hitstruct = undef;
136              
137             my $is_start = $start && defined $self->_words_plugins->{start}->{$word};
138             my $is_end = $end && defined $self->_words_plugins->{end}->{$word};
139             my $is_any = defined $self->_words_plugins->{any}->{$word};
140              
141             if ($is_start) {
142             $begin = 1;
143             $hitstruct = $self->_words_plugins->{start}->{$word};
144             push(@hits,[$begin,$hitstruct]);
145             }
146              
147             if ($is_end) {
148             $begin = 0;
149             $hitstruct = $self->_words_plugins->{end}->{$word};
150             push(@hits,[$begin,$hitstruct]);
151             }
152              
153             if ($is_any) {
154             $begin = 1;
155             $hitstruct = $self->_words_plugins->{any}->{$word};
156             push(@hits,[$begin,$hitstruct]);
157             }
158             }
159              
160             unless ( @hits ) {
161             $self->trace("No hit with","'".$word."'");
162             }
163              
164             # iterate over each type of trigger match for the current word
165             # this allows us to consider combinations of `start`, `end` and `any`
166             # triggers for the current word
167             while (my $hitref = shift @hits) {
168              
169             my ($begin,$hitstruct) = @{$hitref};
170             ######################################################
171             $self->trace("Got a hit with","'".$word."'","!", $begin ? "And it's just the beginning..." : "");
172              
173             # $cnt is the specific position inside our flat array of
174             # positions inside the query.
175             my $pos = $poses[$cnt];
176              
177             # This `for` loop is only executed if we have a partial
178             # match on a trigger phrase i.e. the first word in a `start`
179             # or `any` trigger phrase, or the last word in an `end`
180             # trigger phrase. In this case it iterates through all
181             # those different combination and tries to match it
182             # with the request of the query.
183             #
184             for my $word_count (sort { $b <=> $a } grep { $_ > 1 } keys %{$hitstruct}) {
185             ############################################################
186             $self->trace( "Checking additional multiword triggers with length of", $word_count);
187             my @sofar_words = @{$triggers{$pos}};
188             for my $sofar_word (@sofar_words) {
189             if (defined $hitstruct->{$word_count}->{$sofar_word}) {
190             for (@{$hitstruct->{$word_count}->{$sofar_word}}) {
191             push @results, $self->handle_request_matches($_,$request,$pos);
192             if ($self->return_one && @results) {
193             $self->trace("Got return_one and ".(scalar @results)." results, finishing here");
194             return @results;
195             }
196             }
197             }
198             }
199             # Here we take the index of the partially matched trigger phrase and
200             # calculate where the trigger should start or end (based on whether
201             # it's a `start` or `end` trigger) to verify if the partial match is a
202             # full match against the whole trigger
203             #
204             # Then we check if the next/previous word in the string matches
205             # the next/previous word in the trigger phrase (again, based on whether it's a start or end trigger)
206             my @next_poses_key = grep { $_ >= 0 } $begin ? ($cnt+1)..($cnt+$word_count-1) : ($cnt-$word_count+1)..($cnt-1);
207             my @next_poses = grep { defined $_ && defined $triggers{$_} } @poses[@next_poses_key];
208             @next_poses = reverse @next_poses unless $begin;
209             for my $next_pos (@next_poses) {
210             my @next_triggers = @{$triggers{$next_pos}};
211             my @new_next_words;
212             for my $next_trigger (@next_triggers) {
213             for my $current_sofar_word (@sofar_words) {
214             my $new_next_word = $begin
215             ? join(" ",$current_sofar_word,$next_trigger)
216             : join(" ",$next_trigger,$current_sofar_word);
217             if (defined $hitstruct->{$word_count}->{$new_next_word}) {
218             for (@{$hitstruct->{$word_count}->{$new_next_word}}) {
219             push @results, $self->handle_request_matches($_,$request,( $pos < $next_pos ) ? ( $pos,$next_pos ) : ( $next_pos,$pos ));
220             if ($self->return_one && @results) {
221             $self->trace("Got return_one and ".(scalar @results)." results, finishing here");
222             return @results;
223             }
224             }
225             }
226             push @new_next_words, $new_next_word;
227             }
228             }
229             push @sofar_words, @new_next_words;
230             }
231             }
232              
233             # Check if we have match on a single trigger word
234             if (defined $hitstruct->{1}) {
235             for (@{$hitstruct->{1}}) {
236             push @results, $self->handle_request_matches($_,$request,$poses[$cnt]);
237             if ($self->return_one && @results) {
238             $self->trace("Got return_one and ".(scalar @results)." results, finishing here");
239             return @results;
240             }
241             }
242             }
243             }
244             }
245             }
246             return @results;
247             }
248              
249              
250 0     0 1   sub empty_trigger { croak "empty triggers are not supported by ".__PACKAGE__ }
251              
252             1;
253              
254             __END__