File Coverage

blib/lib/HTML/Breadcrumbs.pm
Criterion Covered Total %
statement 172 178 96.6
branch 83 92 90.2
condition 41 53 77.3
subroutine 20 20 100.0
pod 0 4 0.0
total 316 347 91.0


line stmt bran cond sub pod time code
1             package HTML::Breadcrumbs;
2              
3 7     7   151294 use 5.000;
  7         25  
  7         287  
4 7     7   40 use File::Basename;
  7         15  
  7         3951  
5 7     7   192 use Carp;
  7         19  
  7         575  
6 7     7   519 use strict;
  7         12  
  7         357  
7             require Exporter;
8              
9 7     7   33 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);
  7         12  
  7         19089  
10             $VERSION = '0.7';
11             @ISA = qw(Exporter);
12             @EXPORT = ();
13             @EXPORT_OK = qw(breadcrumbs);
14              
15             my @ARG = qw(path roots indexes omit omit_regex map labels sep format format_last extra);
16              
17             #
18             # Initialise
19             #
20             sub _init
21             {
22 85     85   110 my $self = shift;
23             # Argument defaults
24 85         603 my %arg = (
25             path => $ENV{SCRIPT_NAME},
26             roots => [ '/' ],
27             indexes => [ 'index.html' ],
28             sep => ' > ',
29             format => '%s',
30             format_last => '%s',
31             @_,
32             );
33              
34             # Check for invalid args
35 85         162 my %ARG = map { $_ => 1 } @ARG;
  935         1761  
36 85         284 my @bad = grep { ! exists $ARG{$_} } keys %arg;
  554         1272  
37 85 100       549 croak "[Breadcrumbs::_init] invalid argument(s): " . join(',',@bad) if @bad;
38 83 50 33     290 croak "[Breadcrumbs::_init] 'path' argument must be absolute"
39             if $self->{path} && substr($self->{path},0,1) ne '/';
40              
41             # Add arguments to $self
42 83         719 @$self{ @ARG } = @arg{ @ARG };
43              
44 83         436 return $self;
45             }
46              
47             sub new
48             {
49 85     85 0 394 my $class = shift;
50 85         139 my $self = {};
51 85         190 bless $self, $class;
52 85         205 return $self->_init(@_);
53             }
54              
55             # Identify the root element
56             sub _setup_root
57             {
58 77     77   95 my $self = shift;
59              
60 77 100 66     430 $self->{roots} = [ $self->{roots} ] if $self->{roots} && ! ref $self->{roots};
61 77         97 my $root = '/';
62 77         90 for my $r (sort { length($b) <=> length($a) } @{$self->{roots}}) {
  5         14  
  77         206  
63 77 100       744 if ($self->{path} =~ m/^$r\b/) {
64 74         108 $root = $r;
65 74 100       173 $root .= '/' if substr($root,-1) ne '/';
66 74         132 last;
67             }
68             }
69 77         109 push @{$self->{elt}}, $root;
  77         171  
70 77         168 $self->{root} = $root;
71             }
72              
73             # Setup omit stuff (omit hash, omit_regex arrayrefs)
74             sub _setup_omit
75             {
76 77     77   1113 my $self = shift;
77              
78 77         145 $self->{omit_elt} = {};
79 77         138 $self->{omit_regex_elt} = [];
80 77         798 $self->{omit_regex_path} = [];
81              
82 77 100 100     835 $self->{omit} = [ $self->{omit} ]
83             if $self->{omit} && ! ref $self->{omit};
84             # Create a hash from omit elements
85 77 100 66     835 if ($self->{omit} && ref $self->{omit} eq 'ARRAY') {
86 9         9 for (@{$self->{omit}}) {
  9         20  
87             # Omit elements should be either absolute paths or element basenames
88 11 100       36 if (substr($_,0,1) eq '/') {
    100          
89             # Remove any trailing '/'
90 5 100       10 $_ = substr($_, 0, -1) if substr($_,-1) eq '/';
91             } elsif (m!/!) {
92 1         13 warn "omit arguments must be either absolute paths or simple path basenames - skipping $_";
93 0         0 next;
94             }
95 10         30 $self->{omit_elt}->{$_} = 1;
96             }
97             }
98 76   100     332 my $omit_regex = $self->{omit_regex} || [];
99 76 100       290 $omit_regex = [ $omit_regex ] unless ref $omit_regex eq 'ARRAY';
100             # Create seperate full-path and element omit_regex arrays
101 76         168 for my $o (@$omit_regex) {
102 18 100       45 if ($o =~ m!/!) {
103 7         10 $o =~ s!^\^!!;
104 7         49 $o =~ s!/*(\$)?$!!; #!
105 7         8 push @{$self->{omit_regex_path}}, qq(^$o\$);
  7         33  
106             }
107             else {
108 11         13 push @{$self->{omit_regex_elt}}, $o;
  11         42  
109             }
110             }
111             }
112              
113             # Add path elements to elt array
114             sub _add_elements
115             {
116 76     76   92 my $self = shift;
117 76         121 my $current = $self->{root};
118 76         1921 while ($self->{path} =~ m|^\Q$current\E/*(([^/]+)/?)|) {
119 215         497 my $final = $2;
120 215         368 $current .= $1;
121             # Remove any trailing '/' from current for testing
122 215         271 my $current_test = $current;
123 215 100       537 $current_test = substr($current_test, 0, -1) if substr($current_test, -1) eq '/';
124             # Ignore elements explicitly omitted
125 215 100 100     1209 next if $self->{omit_elt}->{$current_test} || $self->{omit_elt}->{$final};
126             # Ignore elements matching omit_regex_elt patterns
127 205 100       211 next if grep { $final =~ m/$_/ } @{$self->{omit_regex_elt}};
  33         423  
  205         519  
128             # Ignore paths matching omit_regex_path patterns
129 197 100       244 next if grep { $current_test =~ m/$_/ } @{$self->{omit_regex_path}};
  25         220  
  197         436  
130             # Otherwise add to elt array
131 194         207 push @{$self->{elt}}, $current;
  194         4104  
132             }
133             }
134              
135             # Apply element mappings
136             sub _map_elements
137             {
138 2     2   5 my $self = shift;
139 2 50       7 die "invalid map argument" if ref $self->{map} ne 'HASH';
140              
141 2         4 $self->{elt_map} = {};
142 2         5 ELT:
143 2         3 for my $elt (@{$self->{elt}}) {
144 8         8 for my $key (sort keys %{$self->{map}}) {
  8         24  
145             # Map elements must be either absolute paths or element basenames
146 13         15 my $key2 = $key;
147 13 100       30 if (substr($key2,0,1) eq '/') {
    50          
148             # Absolute paths must end in '/'
149 11 100       26 $key2 .= '/' unless substr($key2,-1) eq '/';
150             } elsif ($key2 =~ m!/!) {
151 0         0 warn "map arguments must be either absolute paths or simple path basenames - skipping $key2";
152 0         0 next;
153             }
154              
155             # If the map key matches this element, record map value in elt_map
156 13 100       45 my $match = ($key2 =~ m!/!) ? $elt eq $key2 : $elt =~ m,/\Q$key2\E/$,;
157 13 100       35 if ($match) {
158 4         11 $self->{elt_map}->{$elt} = $self->{map}->{$key};
159 4         10 next ELT;
160             }
161             }
162             }
163             }
164              
165             # Check the final element for indexes
166             sub _check_final_index_element
167             {
168 76     76   95 my $self = shift;
169              
170 76 100 66     1244 $self->{indexes} = [ $self->{indexes} ]
171             if $self->{indexes} && ! ref $self->{indexes};
172 76 50       197 if (ref $self->{indexes} eq 'ARRAY') {
173             # Convert indexes to hash
174 76         82 my %indexes = map { $_ => 1 } @{$self->{indexes}};
  79         281  
  76         183  
175             # Check final element
176 76         131 my $final = basename($self->{elt}->[ $#{$self->{elt}} ]);
  76         2317  
177 76 100       306 if ($indexes{$final}) {
178 4         4 pop @{$self->{elt}};
  4         11  
179             }
180             }
181             }
182              
183             #
184             # Split the path into elements (stored in $self->{elt} arrayref)
185             #
186             sub _split
187             {
188 77     77   94 my $self = shift;
189 77         162 $self->{elt} = [];
190              
191             # Identify the root
192 77         162 $self->_setup_root;
193              
194             # Setup omit stuff
195 77         164 $self->_setup_omit;
196              
197             # Add path elements to elt array
198 76         170 $self->_add_elements;
199              
200             # Apply element mappings
201 76 100       202 $self->_map_elements if $self->{'map'};
202              
203             # Check for final index elements
204 76         198 $self->_check_final_index_element;
205              
206             }
207              
208             #
209             # Generate a default label for $elt
210             #
211             sub _label_default
212             {
213 250     250   277 my $self = shift;
214 250         333 my ($elt, $last, $extra) = @_;
215 250         274 my $label = '';
216              
217 250 100 66     1158 if ($elt eq '/' || $elt eq '') {
218 71         104 $label = 'Home';
219             }
220             else {
221 179 100       454 $elt = substr($elt,0,-1) if substr($elt,-1) eq '/';
222 179         3930 $label = basename($elt);
223 179 100       508 $label =~ s/\.[^.]*$// if $last;
224 179 100 100     1226 $label = ucfirst($label) if lc($label) eq $label && $label =~ m/^\w+$/;
225             }
226              
227 250         814 return $label;
228             }
229              
230             #
231             # Return a label for the given element
232             #
233             sub _label
234             {
235 266     266   311 my $self = shift;
236 266         380 my ($elt, $last, $extra) = @_;
237 266         299 my $label = '';
238              
239             # Check $self->{labels}
240 266 100       2087 if (ref $self->{labels} eq 'CODE') {
    100          
241 12 100 100     62 $elt = substr($elt,0,-1) if substr($elt,-1) eq '/' && $elt ne '/';
242 12         311 $label = $self->{labels}->($elt, basename($elt), $last, $extra);
243             }
244             elsif (ref $self->{labels} eq 'HASH') {
245 40 100 100     499 $elt = substr($elt,0,-1) if substr($elt,-1) eq '/' && $elt ne '/';
246 40   66     312 $label ||= $self->{labels}->{$elt};
247 40 100 100     534 $label ||= $self->{labels}->{$elt . '/'} unless $elt eq '/' || $last;
      100        
248 40   100     1736 $label ||= $self->{labels}->{basename($elt)};
249             }
250              
251             # Else use defaults
252 266   66     965 $label ||= $self->_label_default($elt, $last, $extra);
253              
254 266         600 return $label;
255             }
256              
257             #
258             # Render the elt path for URI use, and lookup in elt_map if applicable
259             #
260             sub _uri_elt
261             {
262 190     190   422 my $self = shift;
263 190         421 local $_ = shift;
264 190 100       507 $_ = $self->{elt_map}->{$_} if exists $self->{elt_map}->{$_};
265             # URI escape - should maybe use URI::Escape here instead
266 190         288 s/ /%20/g;
267 190         1057 return $_;
268             }
269              
270             #
271             # HTML-format the breadcrumbs
272             #
273             sub _format
274             {
275 76     76   94 my $self = shift;
276              
277 76         77 my $out;
278 76         101 for (my $i = 0; $i <= $#{$self->{elt}}; $i++) {
  342         1871  
279              
280             # Format breadcrumb links
281 266 100       471 if ($i != $#{$self->{elt}}) {
  266         716  
282             # Generate label
283 190         532 my $label = $self->_label($self->{elt}->[$i], undef, $self->{extra});
284              
285             # $self->{format} coderef
286 190 100 33     1342 if (ref $self->{format} eq 'CODE') {
    50          
287 6         16 $out .= $self->{format}->($self->_uri_elt($self->{elt}->[$i]),
288             $label, $self->{extra});
289             }
290             # $self->{format} sprintf pattern
291             elsif ($self->{format} && ! ref $self->{format}) {
292 184         486 $out .= sprintf $self->{format}, $self->_uri_elt($self->{elt}->[$i]),
293             $label;
294             }
295             # Else croak
296             else {
297 0         0 croak "[Breadcrumbs::format] invalid format $self->{format}";
298             }
299              
300             # Separator
301 190         523 $out .= $self->{sep};
302             }
303              
304             # Format final element breadcrumb label
305             else {
306             # Generate label
307 76         212 my $label = $self->_label($self->{elt}->[$i], 'last', $self->{extra});
308              
309             # $self->{format_last} coderef
310 76 100 33     431 if (ref $self->{format_last} eq 'CODE') {
    50          
311 2         8 $out .= $self->{format_last}->($label, $self->{extra});
312             }
313             # $self->{format_last} sprintf pattern
314             elsif ($self->{format_last} && ! ref $self->{format_last}) {
315 74         208 $out .= sprintf $self->{format_last}, $label;
316             }
317             # Else croak
318             else {
319 0         0 croak "[Breadcrumbs::format] invalid format_last $self->{format_last}";
320             }
321             }
322             }
323              
324 76         897 return $out;
325             }
326              
327             #
328             # The real work - process and render the given path
329             #
330             sub render
331             {
332 83     83 0 103 my $self = shift;
333 83         135 my %arg = @_;
334              
335             # Check for invalid args
336 83         123 my %ARG = map { $_ => 1 } @ARG;
  913         1501  
337 83         208 my @bad = grep { ! exists $ARG{$_} } keys %arg;
  0         0  
338 83 50       185 croak "[Breadcrumbs::render] invalid argument(s): " . join(',',@bad) if @bad;
339              
340             # Add args to $self
341 83         146 for (@ARG) {
342 913 50       1777 $self->{$_} = $arg{$_} if defined $arg{$_};
343             }
344              
345             # Croak if no path
346 83 100       1320 croak "[Breadcrumbs::render] no valid 'path' found" if ! $self->{path};
347 80 100       703 croak "[Breadcrumbs::render] 'path' argument must be absolute"
348             if substr($self->{path},0,1) ne '/';
349              
350             # Split the path into elements
351 77         166 $self->_split();
352              
353             # Format
354 76         166 return $self->_format();
355             }
356              
357             #
358             # Alias for render
359             #
360             sub to_string
361             {
362 2     2 0 5 my $self = shift;
363 2         3 $self->render(@_);
364             }
365              
366             #
367             # Procedural interface
368             #
369             sub breadcrumbs
370             {
371 73     73 0 6387 my $bc = HTML::Breadcrumbs->new(@_);
372 72 50       215 croak "[breadcrumbs] object creation failed!" if ! ref $bc;
373 72         164 return $bc->render();
374             }
375              
376             1;
377              
378             __END__