File Coverage

blib/lib/HTML/TrackerLink.pm
Criterion Covered Total %
statement 94 113 83.1
branch 37 64 57.8
condition 4 6 66.6
subroutine 16 18 88.8
pod 7 7 100.0
total 158 208 75.9


line stmt bran cond sub pod time code
1             package HTML::TrackerLink;
2              
3             # HTML::TrackerLink is a package for automatically finding tracker references
4             # in the form of 'Keyword 12345' or '#12345', and converting them into links into
5             # the external tracking system.
6              
7             # See POD below for more details
8              
9 2     2   24095 use 5.006;
  2         9  
  2         78  
10 2     2   12 use strict;
  2         6  
  2         118  
11              
12 2     2   22 use vars qw{$VERSION $errstr};
  2         3  
  2         178  
13             BEGIN {
14 2     2   4 $VERSION = '1.03';
15 2         3812 $errstr = '';
16             }
17              
18              
19              
20              
21              
22             #####################################################################
23             # Constructor and Accessors
24              
25             sub new {
26 3     3 1 2436 my $class = shift;
27              
28             # Create our object
29 3         16 my $self = bless {
30             keywords => {},
31             }, $class;
32 3 100       16 return $self unless @_;
33              
34             # Handle arguments
35 1 50       11 if ( @_ == 1 ) {
    50          
    50          
36             # Should be a tracker URL for the default search
37 0         0 my $url = shift;
38 0 0       0 return undef unless $self->_check_url( $url );
39              
40             # Set the default search
41 0         0 $self->{default} = $url;
42              
43             } elsif ( @_ == 2 ) {
44             # Should be a single keyword/url pair
45 0         0 my ($keyword, $url) = @_;
46 0 0       0 return undef unless $self->_check_keyword( $keyword );
47 0 0       0 return undef unless $self->_check_url( $url );
48              
49             # Set the keyword
50 0         0 $self->{keywords}->{lc $keyword} = $url;
51              
52             # Set the default search to be the same
53 0         0 $self->{default_keyword} = $keyword;
54              
55             } elsif ( scalar(@_) % 2 == 0 ) {
56             # Multiple keyword/url pairs
57 1         5 my %keywords = @_;
58 1         9 foreach my $keyword ( sort keys %keywords ) {
59 2         6 my $url = $keywords{$keyword};
60 2 50       14 unless ( $self->_check_keyword( $keyword ) ) {
61 0         0 return $self->_error( "Invalid keyword '$keyword': "
62             . $self->errstr );
63             }
64 2 50       7 unless ( $self->_check_url( $url ) ) {
65 0         0 return $self->_error( "Bad URL for keyword '$keyword': "
66             . $self->errstr );
67             }
68              
69             # Set the keyword
70 2         44 $self->{keywords}->{$keyword} = $url;
71             }
72              
73             } else {
74 0         0 return $self->_error( 'Arguments must be in keyword/url pairs' );
75             }
76              
77 1         5 $self;
78             }
79              
80             # Return the currently defined keywords
81             sub keywords {
82 10 100   10 1 20 if ( wantarray ) {
83 5         9 return sort keys %{ $_[0]->{keywords} };
  5         38  
84             } else {
85 5         7 return scalar keys %{ $_[0]->{keywords} };
  5         23  
86             }
87             }
88              
89              
90              
91              
92              
93             #####################################################################
94             # Main Methods
95              
96             # Get or set a keyword search
97             sub keyword {
98 2     2 1 9 my $self = shift;
99 2 50       7 my $keyword = $self->_check_keyword($_[0]) ? lc shift : return undef;
100 2 50       6 return $self->{keywords}->{$keyword} unless @_;
101              
102             # Set the tracker URL
103 2 50       6 my $url = $self->_check_url($_[0]) ? shift : return undef;
104 2         8 $self->{keywords}->{$keyword} = $url;
105             }
106              
107             # Get the current default search
108             sub default {
109 11     11 1 1307 my $self = shift;
110 11 100       69 return $self->{default_keyword}
    100          
111             ? $self->{keywords}->{ $self->{default_keyword} }
112             : $self->{default} unless @_;
113              
114             # Try to set the default search
115 1 50       4 my $url = $self->_check_url($_[0]) ? shift : return undef;
116              
117             # In case they are using a keyword, remove it
118 1         7 delete $self->{default_keyword};
119 1         7 $self->{default} = $url;
120             }
121              
122             # Make the default search the same as a particular keyword
123             sub default_keyword {
124 1     1 1 5 my $self = shift;
125 1 50       4 my $keyword = $self->_check_keyword($_[0]) ? lc shift : return undef;
126              
127             # Does the keyword exist?
128 1 50       5 unless ( exists $self->{keywords}->{$keyword} ) {
129 0         0 return $self->_error( "The keyword '$keyword' does not exist" );
130             }
131              
132             # In case they are using an explicit default search, remove it
133 1         2 delete $self->{default};
134              
135 1         4 $self->{default_keyword} = $keyword;
136             }
137              
138             # Process and return a string
139             sub process {
140 5     5 1 2764 my $self = shift;
141 5 50 33     37 my $text = (@_ and defined $_[0]) ? shift
142             : return $self->_error( 'You did not provide a string to process' );
143              
144             # Prepare the transforms
145 5         8 my @replace = ();
146 5 50       17 if ( $self->keywords ) {
147 5         12 my $any_keyword = '(?:' . join('|', $self->keywords) .')';
148             push @replace, [
149             qr/\b($any_keyword)\s+\#?(\d+)/is,
150 4     4   22 sub { $self->_replace(
151             $self->{keywords}->{lc $_[3]}, $_[2], $_[4],
152             ) },
153 5         152 ];
154             }
155 5 100       52 if ( $self->default ) {
156             push @replace, [
157             qr/\#(\d+)/s,
158 5     5   12 sub { $self->_replace(
159             $self->default, $_[2], $_[3],
160             ) },
161 4         23 ];
162             }
163              
164             # Hand off to the main substitution method
165 5         18 return $self->_subst( $text, @replace );
166             }
167              
168             # Implement the parallel substitution
169             sub _subst {
170 5     5   9 my $self = shift;
171 5         11 my $input = shift;
172              
173             # Map the match regex to capture everything BEFORE the match,
174             # and the entire pattern provided.
175             # (We'll provide them as the first params)
176 5         8 my @try = map { [ qr/\G(.*?)($_->[0])/s => $_->[1] ] } @_;
  9         307  
177 5 50       13 unless ( @try ) {
178             # Handle the pathological no-replace case
179 0         0 return $input;
180             }
181              
182             # Start the main loop
183 5         9 my $position = 0;
184 5         6 my $len = length $input;
185 5         8 my $output = '';
186 5         15 while ( $position < $len ) {
187 13         17 my $found = undef;
188 13         41 my @start = ();
189 13         17 my @end = ();
190 13         20 foreach my $r ( @try ) {
191             # Skip if it is not in the string
192 24         54 pos $input = $position;
193 24 100       195 next unless $input =~ $r->[0];
194              
195             # Skip if it DOESN'T match earlier
196 14 100 100     53 if ( $found and $start[2] <= $-[2] ) {
197 3         11 next;
198             }
199              
200             # This is the best option.
201             # Save the matching regex
202 11         19 $found = $r->[1];
203 11         60 @start = @-;
204 11         72 @end = @+;
205             }
206              
207             # Break out if no more matches
208 13 100       35 last unless $found;
209              
210             # Append the pre-match string to the output
211 9         22 $output .= substr( $input, $start[1], $end[1] - $start[1] );
212              
213             # Pass the rest to the transform function
214 40         95 my $rv = $found->(
215             map {
216 9         21 substr( $input, $start[$_], $end[$_] - $start[$_] )
217             } 0 .. $#end
218             );
219 9 50       30 unless ( defined $rv ) {
220             # Transform is signaling an error
221 0         0 return undef;
222             }
223              
224             # Transform completed ok
225 9         14 $output .= $rv;
226              
227             # Move the match position for the next iteration
228 9         596 $position = $end[2];
229             }
230              
231             # Append the remainder of the string
232 5         66 return $output . substr( $input, $position );
233             }
234              
235             # Return any error message
236 0     0 1 0 sub errstr { $errstr }
237              
238              
239              
240              
241              
242             #####################################################################
243             # Support Methods
244              
245             sub _check_keyword {
246 5     5   7 my $self = shift;
247 5 50       14 my $kw = shift or return $self->_error( 'You did not provide a keyword' );
248 5 50       16 return $self->_error( 'Keyword contains non-word characters' ) if $kw =~ /\W/;
249 5 50       16 return $self->_error( 'Keyword cannot start with a number' ) if $kw =~ /^\d/;
250 5         17 return 1;
251             }
252              
253             sub _check_url {
254 5     5   7 my $self = shift;
255 5 50       30 my $url = shift or return $self->_error( 'You did not provide a tracker URL' );
256 5 50       24 unless ( $url =~ m!^https?://[\w.]+/! ) {
257 0         0 return $self->_error( 'The tracker URL format appears to be invalid' );
258             }
259 5 50       14 unless ( $url =~ /\%n/ ) {
260 0         0 return $self->_error( 'The tracker URL does not contain a %n placeholder' );
261             }
262 5         17 return 1;
263             }
264              
265             # Generates the link in the replacer
266             sub _replace {
267 9     9   23 my ($self, $url, $text, $id) = @_;
268              
269             # Create the link
270 9         31 $url =~ s/\%n/$id/g;
271 9         30 return "$text";
272             }
273              
274 0     0     sub _error { $errstr = $_[1]; undef }
  0            
275              
276             1;
277              
278             __END__