File Coverage

blib/lib/Blog/Blosxom.pm
Criterion Covered Total %
statement 33 222 14.8
branch 0 72 0.0
condition 0 49 0.0
subroutine 11 27 40.7
pod 12 12 100.0
total 56 382 14.6


line stmt bran cond sub pod time code
1             package Blog::Blosxom;
2              
3 1     1   29635 use warnings;
  1         3  
  1         40  
4 1     1   7 use strict;
  1         2  
  1         39  
5              
6 1     1   1186 use FindBin;
  1         1522  
  1         48  
7 1     1   1232 use FileHandle;
  1         14500  
  1         7  
8 1     1   448 use File::Find;
  1         2  
  1         63  
9 1     1   6 use File::Spec;
  1         5  
  1         20  
10 1     1   1061 use File::stat;
  1         15206  
  1         10  
11 1     1   1436 use Time::localtime;
  1         3053  
  1         72  
12              
13 1     1   9 use List::Util qw(min);
  1         2  
  1         1370  
14              
15             =head1 NAME
16              
17             Blog::Blosxom - A module version of the apparently inactive blosxom.cgi
18              
19             =head1 VERSION
20              
21             Version 0.01
22              
23             =cut
24              
25             our $VERSION = '0.10';
26              
27             =head1 SYNOPSIS
28              
29             use Blog::Blosxom;
30              
31             ...
32              
33             my $blog = Blog::Blosxom->new(%params);
34             $blog->run($path, $flavour);
35              
36             A path comes in from somewhere - usually an HTTP request. This is applied to the
37             blog directory. A configurable file extension is added to the path and if it matches
38             a file that file is served as the matched entry. If the path matches a directory,
39             all entries in that directory and in subdirectories are served. If the path looks
40             like a date, all posts that match that date are served. A string is returned, which
41             is usually printed back to CGI. The string is the matched entries, served in the
42             specified output format, or flavour. The flavour is determined by the file extension
43             on the incoming path, or a GET parameter, or a configured default.
44              
45             =head1 DESCRIPTION
46              
47             Blosxom is a blog engine. It is a rewrite of a CGI script found at www.blosxom.com.
48             Blosxom uses the filesystem as the database for blog entries. Blosxom's run()
49             method takes two parameters: the path and the flavour.
50              
51             The CGI script that ships with the module is an example of how to use this module
52             to reproduce the behaviour of the original Blosxom script, but the idea here is
53             that it is up to you how to get this data.
54              
55             Every file that ends in a configurable file extension and is placed somewhere within
56             the blog root directory is, in the default situation, served up on a big page, in
57             date order, having been processed and turned into blog posts. The set of files chosen
58             for display is pared down by specifying a path to limit the set to only entries under
59             that path, the narrowest possible filter of course being when the path actually
60             matches a single blog entry.
61              
62             Alternatively, the path may be a date in the format YYYY[/MM[/DD]], with the brackets
63             denoting an optional section. This will be used to filter the posts by date instead
64             of by location. All posts under the blog root are candidates for being returned. A
65             TODO is to concatenate a date-style filter and a directory-style filter.
66              
67             The module is designed to be extensible. That is, there are several methods in it
68             that are designed to be overridden by subclasses to extend the functionality of
69             Blog::Blosxom. You can see the L section below for details on these, and
70             examples.
71              
72             =head1 TERMS
73              
74             =head2 entry
75              
76             Entry is used to mean both the individual article (its title, content and any
77             metadata) and the file that contains that data. The entry is a filename with a
78             customisable file extension. The file name and file extension have two differnent
79             purposes.
80              
81             The file extension is used to decide what is a blog post and what isn't. Files that
82             have the defined file extension will be found by the blog engine and displayed, so
83             long as they are within the filter.
84              
85             The entry's filename is used to find a single entry. The path you provide to
86             C is given the file extension defined for blog entries and then applied to the
87             root directory. If this is a file, that is served. If not, it is tested without the
88             file extension to be a directory. If it is a directory, all files ending with this
89             extension and within that directory are served.
90              
91             =head2 story
92              
93             The story is the formatted version of an entry. The file story.$flavour is used to
94             insert the various parts of the blog entry into a template, which is then concatenated
95             to a string which is itself returned from the C method. See below for what I
96             mean by $flavour.
97              
98             =head2 flavour
99              
100             The flavour of the blog is simply the format in which the blog entry is served as a
101             story. The flavour is determined by you. The CGI script takes the file extension
102             from the request URI, or the C GET parameter.
103              
104             If neither is provided, the default flavour is used. This is passed as a parameter
105             to C and defaults to C.
106              
107             =head2 component
108              
109             A component is one of the five (currently) sections of the page: head, foot, story,
110             date and content-type. The story and date components appear zero-to-many times on
111             each page and the other three appear exactly once.
112              
113             =head2 template
114              
115             A template is a flavoured component. It is defined as a file in the blog root whose
116             filename is the component and whose extension is the flavour. E.g. C is
117             the HTML-flavoured head component's template.
118              
119             =head1 METHODS
120              
121             =head2 new(%params)
122              
123             Create a new blog. Parameters are provided in a hash and are:
124              
125             =over
126              
127             =item blog_title
128              
129             This will be available in your templates as $blog_title, and by default appears in the
130             page title and at the top of the page body.
131              
132             This parameter is required.
133              
134             =item blog_description
135              
136             This will be available in your templates as $blog_description. This does not appear in
137             the default templates.
138              
139             =item blog_language
140              
141             This is used for the RSS feed, as well as any other flavours you specify that have a
142             language parameter.
143              
144             =item datadir
145              
146             This is where blosxom will look for the blog's entries. A relative path will be relative
147             to the script that is using this module.
148              
149             This parameter is required.
150              
151             =item url
152              
153             This will override the base URL for the blog, which is automatic if you do not provide.
154              
155             =item depth
156              
157             This is how far down subdirectories of datadir to look for more blog entries. 0 is the
158             default, which means to look down indefinitely. 1, therefore, means to look only in the
159             datadir itself, up to n, which will look n-1 subdirectories down.
160              
161             =item num_entries
162              
163             This is the maximum number of stories to display when multiple are found in the filter.
164              
165             =item file_extension
166              
167             By default, Blosxom will treat .txt files as blog entries. Change this to use a
168             different file extension. Do not provide the dot that separates the filename and the
169             file extension.
170              
171             =item default_flavour
172              
173             The flavour simply determines which set of templates to use to draw the blog. This
174             defines which flavour to use by default. Vanilla blosxom has HTML and RSS flavours,
175             of which RSS sucks really hard so really only HTML is available by default.
176              
177             =item show_future_entries
178              
179             This is a bit of a strange one, since by default, having a future date on an entry
180             is a filesystem error, but if you want to override how the date of a template is
181             defined, this will be helpful to you.
182              
183             =item plugin_dir
184              
185             Tells blosxom where to look for plugins. This is empty by default, which means it won't
186             look for plugins. Relative paths will be taken relative to the script that uses this
187             module.
188              
189             =item plugin_state_dir
190              
191             Some plugins wish to store state. This is where the state data will be stored. It will
192             need to be writable. Defaults to plugin_dir/state if you specify a plugin_dir.
193              
194             =item static_dir
195              
196             Blosxom can publish your files statically, which means you run the script and it creates
197             HTML files (for example) for each of your entries, instead of loading them dynamically.
198             This defines where those files should go. I haven't actually implemented this because I
199             don't really want to.
200              
201             =item static_password
202              
203             You have to provide a password if you want to use static rendering, as a security
204             measure or something.
205              
206             =item static_flavours
207              
208             An arrayref of the flavours that Blosxom should generate statically. By default this is
209             html and rss.
210              
211             =item static_entries
212              
213             Set this to a true value to turn on static generation of individual entries. Generally
214             there is no point because your entries are static files already, but you may be using a
215             plugin to alter them before rendering.
216              
217             =back
218              
219             =cut
220              
221             sub new {
222 0     0 1   my ($class, %params) = @_;
223              
224 0           for (qw(blog_title datadir)) {
225 0 0 0       die "Required parameter $_ not provided to Blog::Blosxom->new"
226             unless exists $params{$_} && $params{$_};
227             }
228              
229 0 0         die $params{datadir} . " does not exist!" unless -d $params{datadir};
230              
231 0           my %defaults = (
232             blog_description => "",
233             blog_language => "en",
234             url => "",
235             depth => 0,
236             num_entries => 40,
237             file_extension => "txt",
238             default_flavour => "html",
239             show_future_entries => 0,
240             plugin_dir => "",
241             plugin_state_dir => "",
242             static_dir => "",
243             static_password => "",
244             static_flavours => [qw(html rss)],
245             static_entries => 0,
246             require_namespace => 0,
247             );
248              
249 0           %params = (%defaults, %params);
250              
251 0 0 0       $params{plugin_state_dir} ||= $params{plugin_dir} if $params{plugin_dir};
252              
253             # Absolutify relative paths
254 0           for my $key (qw(plugin_dir datadir static_dir plugin_state_dir)) {
255 0           my $dir = $params{$key};
256              
257 0 0         unless (File::Spec->file_name_is_absolute( $dir )) {
258 0           $dir = File::Spec->catdir($FindBin::Bin, $dir);
259             }
260              
261 0           $params{$key} = $dir;
262             }
263              
264 0           my $self = bless \%params, $class;
265 0           $self->_load_plugins;
266 0           $self->_load_templates;
267              
268 0           return $self;
269             }
270              
271             =head2 run ($path, $flavour)
272              
273             It is now the responsibility of the user to provide the correct path and
274             flavour. That is because there are several ways that you can gain this
275             information, and it is not up to this engine to decide what they are. That is,
276             this information comes from the request URL and, possibly, the parameter string,
277             POST, cookies, what-have-you.
278              
279             Therefore:
280              
281             =over
282              
283             =item
284              
285             The path is the entire path up to the filename. The filename shall not include
286             a file extension. The filename is optional, and if omitted, the directory given
287             will be searched for all entries and an index page generated.
288              
289             =item
290              
291             The flavour can be gathered in any manner you desire. The original Blosxom
292             script would use either a parameter string, C, or simply by using
293             the flavour as the file extension for the requested path.
294              
295             =back
296              
297             No flavour provided will result in the default being used, obviously. No path
298             being provided will result in the root path being used, since these are
299             equivalent.
300              
301             =cut
302              
303             sub run {
304 0     0 1   my ($self, $path, $flavour) = @_;
305              
306 0   0       $path ||= "";
307 0   0       $flavour ||= $self->{default_flavour};
308              
309 0           $path =~ s|^/||;
310              
311 0           my @entries;
312              
313 0           $self->{path_info} = $path;
314 0           $self->{flavour} = $flavour;
315              
316             # Build an index page for the path
317 0           @entries = $self->entries_for_path($path);
318 0           @entries = $self->filter(@entries);
319 0           @entries = $self->sort(@entries);
320              
321 0           @entries = @entries[0 .. min($#entries, $self->{num_entries}-1) ];
322              
323 0           $self->{entries} = [];
324              
325             # A special template. The user is going to need to know this, but not print it.
326 0           $self->{content_type} = $self->template($path, "content_type", $flavour);
327              
328 0           my @templates;
329              
330 0           my $date = "";
331 0           for my $entry (@entries) {
332             # TODO: Here is an opportunity to style the entries in the style
333             # of the subdir they came from.
334 0           my $entry_data = $self->entry_data($entry);
335 0           push @{$self->{entries}}, $entry_data;
  0            
336              
337 0           my $entry_date = join " ", @{$entry_data}{qw(da mo yr)}; # To create a date entry when it changes.
  0            
338              
339 0 0         if ($date ne $entry_date) {
340 0           $date = $entry_date;
341 0           my $date_data = { map { $_ => $entry_data->{$_} } qw( yr mo mo_num dw da hr min ) };
  0            
342              
343 0           push @templates, $self->interpolate($self->template($path, "date", $flavour), $date_data);
344             }
345              
346 0           push @templates, $self->interpolate($self->template($path, "story", $flavour), $entry_data);
347             }
348              
349             # If we do head and foot last, we let plugins use data about the contents in them.
350 0           unshift @templates, $self->interpolate($self->template($path, "head", $flavour), $self->head_data());
351 0           push @templates, $self->interpolate($self->template($path, "foot", $flavour), $self->foot_data());
352              
353             # A skip plugin will stop processing just before anything is output.
354             # Not sure why.
355 0 0         return if $self->_check_plugins('skip');
356              
357 0           return join "\n", @templates;
358             }
359              
360             =head2 template($path, $component, $flavour)
361              
362             Returns a chunk of markup for the requested component in the requested flavour
363             for the requested path. The path will be the one given to C.
364              
365             By default the template file chosen is the file C<$component.$flavour> within
366             the C<$path> provided, and if not found, upwards from there to the blog root.
367              
368             The templates used are I, I, I, I and I,
369             so the HTML template for the head would be C.
370              
371             =cut
372              
373             sub template {
374 0     0 1   my ($self, $path, $comp, $flavour) = @_;
375              
376 0           my $template;
377              
378 0 0         unless ($template = $self->_check_plugins('template', @_)) {
379 0           my $fn = File::Spec->catfile($self->{datadir}, $path, "$comp.$flavour");
380              
381 0           while (1) {
382             # Return the contents of this template if the file exists. If it is empty,
383             # we have defaults set up.
384 0 0         if (-e $fn) {
385 0           open my $fh, "<", $fn;
386 0           $template = join '', <$fh>;
387             }
388              
389             # Stop looking when there is no $path to go between datadir and the
390             # template file. For portability, we can't check whether it is a "/"
391 0 0 0       last if !$path or $path eq File::Spec->rootdir;
392              
393             # Look one dir higher and go again.
394 0           my @dir = File::Spec->splitdir($path);
395 0           pop @dir;
396 0           $path = File::Spec->catdir(@dir);
397 0           $fn = File::Spec->catfile($self->{datadir}, $path, "$comp.$flavour");
398             }
399             }
400              
401 0   0       $template ||= $self->{template}{$flavour}{$comp} || $self->{template}{error}{$comp};
      0        
402              
403 0           return $template;
404             }
405              
406             =head2 entries_for_path
407              
408             Given a path, find the entries that should be returned. This may be overridden
409             by a plugin defining the function "entries", or this "entries_for_path" function.
410             They are synonymous. See L for information on overriding this method.
411              
412             The path will not include C.
413              
414             It implements two behaviours. If the path requested is a real path then it is
415             searched for all blog entries, honouring the depth parameter that limits how far
416             below the C we should look for blog entries.
417              
418             If it is not then it is expected to be a date, being in 1, 2 or 3 parts, in one
419             true date ISO format. This version will return all entries filtered by this date
420             specification. See also L, which determines the date on which the
421             post was made and can be overridden in plugins.
422              
423             =cut
424              
425             sub entries_for_path {
426 0     0 1   my ($self, $path) = @_;
427            
428 0           my @entries;
429              
430 0 0         return @entries if @entries = $self->_check_plugins('entries', @_);
431              
432 0           my $abs_path = File::Spec->catdir( $self->{datadir}, $path );
433              
434             # If this is an entry, return it.
435 0 0         if (-f $abs_path . "." . $self->{file_extension}) {
436 0           my $date = $self->date_of_post($abs_path . "." . $self->{file_extension});
437 0           return [ $path . "." . $self->{file_extension}, { date => $date } ];
438             }
439              
440 0 0         if (-d $abs_path) {
441             # We use File::Find on a real path
442             my $find = sub {
443 0 0   0     return unless -f;
444              
445 0           my $rel_path = File::Spec->abs2rel( $File::Find::dir, $self->{datadir} );
446 0           my $curdepth = File::Spec->splitdir($rel_path);
447              
448 0           my $fex = $self->{file_extension};
449              
450             # not specifying a file extension is a bit silly.
451 0 0 0       if (!$fex || /\.$fex$/) {
452 1     1   8 no warnings "once"; # File::Find::name causes a warning.
  1         2  
  1         889  
453              
454 0           my $rel_file = File::Spec->catfile( $rel_path, $_ );
455 0           $rel_file = File::Spec->canonpath($rel_file); # This removes any artefacts like ./
456 0           my $date = $self->date_of_post($File::Find::name);
457 0           my $file_info = { date => $date };
458              
459 0           push @entries, [$rel_file, $file_info ];
460             }
461              
462 0   0       $File::Find::prune = ($self->{depth} && $curdepth > $self->{depth});
463 0           };
464              
465 0           File::Find::find( $find, $abs_path );
466             }
467             else {
468             # We use date stuff on a fake path.
469             # TODO: see whether we can split the path into a date section and a real section.
470 0           my @ymd = File::Spec->splitdir( $path );
471 0           my @all_entries = $self->entries_for_path( "" );
472              
473 0           my @entries = grep {
474 0           my $post_date = localtime( $_->[1]{date} );
475            
476             # requested year
477 0 0 0       ($ymd[0] == ($post_date->year+1900))
    0 0        
      0        
478             and
479             # matches month, or moth not requested
480             (!$ymd[1] || $ymd[1] == ($post_date->mon+1))
481             and
482             # matches day, or day not rquested
483             (!$ymd[2] || $ymd[2] == $post_date->mday)
484              
485             ? $_ : ()
486              
487             } @all_entries;
488             }
489              
490 0           return @entries;
491             }
492              
493             =head2 date_of_post ($fn)
494              
495             Return a unix timestamp defining the date of the post. The filename provided to
496             the method is an absolute filename.
497              
498             =cut
499              
500             sub date_of_post {
501 0     0 1   my ($self, $fn) = @_;
502              
503 0           my $dop;
504 0 0         return $dop if $dop = $self->_check_plugins("date_of_post", @_);
505              
506 0           return stat($fn)->mtime;
507             }
508              
509             =head2 filter (@entries)
510              
511             This function returns only the desired entries from the array passed in. By
512             default it just returns the array back, so is just a place to check for plugins.
513              
514             This can be overridden by plugins in order to alter the way the module filters
515             the files. See L for more details.
516              
517             =cut
518              
519             sub filter {
520 0     0 1   my ($self, @entries) = @_;
521              
522 0           my @remaining_files = $self->_check_plugins("filter", @_);
523              
524 0   0       return @remaining_files || @entries;
525             }
526              
527             =head2 sort (@entries)
528              
529             Sort @entries and return the new list.
530              
531             Default behaviour is to sort by date.
532              
533             =cut
534              
535             sub sort {
536 0     0 1   my ($self, @entries) = @_;
537              
538 0           my @sorted_entries;
539 0 0         return @sorted_entries if @sorted_entries = $self->_check_plugins("sort", @_);
540              
541 0           @sorted_entries = sort { $a->[1]->{date} <=> $b->[1]->{date} } @entries;
  0            
542 0           return @sorted_entries;
543             }
544              
545             =head2 static_mode($password, $on)
546              
547             Sets static mode. Pass in the password to turn it on. Turns it off if it is already on.
548              
549             =cut
550              
551             sub static_mode {
552 0     0 1   my ($self, $password, $on) = @_;
553              
554 0 0         die "No static dir defined" unless $self->{static_dir};
555 0 0         die "No static publishing password defined" unless $self->{static_password};
556              
557             # Set it to toggle if we don't specify.
558 0 0         $on = !$self->{static_mode} if !defined $on;
559              
560 0 0 0       if ($self->{static_mode} && !$on) {
561 0           $self->{static_mode} = 0;
562 0           $blosxom::static_or_dynamic = 'dynamic';
563 0           return;
564             }
565            
566 0 0 0       if ($on && $password eq $self->{static_password}) {
567 0           $self->{static_mode} = 1;
568 0           $blosxom::static_or_dynamic = 'static';
569             }
570             }
571              
572             =head2 interpolate($template, $extra_data)
573              
574             Each template is interpolated, which means that variables are swapped out if
575             they exist. Each template may have template-specific variables; e.g. the story
576             template has a title and a body. Those are provided in the extra data, which is
577             a hashref with the variable name to be replaced (without the $) as the key, and
578             the corresponding value as the value.
579              
580             By default, a different set of variables are available to each template:
581              
582             =head3 All templates
583              
584             These are defined by you when you provide them to new() or run()
585              
586             =over
587              
588             =item blog_title
589            
590             =item blog_description
591              
592             =item blog_language
593              
594             =item url
595              
596             =item path_info
597              
598             =item flavour
599              
600             =back
601              
602             =head3 Story (entry) template
603              
604             These are defined by the entry.
605              
606             =over
607              
608             =item title
609              
610             Post title
611              
612             =item body
613              
614             The body of the post
615              
616             =item yr
617              
618             =item mo
619              
620             =item mo_num
621              
622             =item da
623              
624             =item dw
625              
626             =item hr
627              
628             =item min
629              
630             Timestamp of entry. mo = month name; dw = day name
631              
632             =item path
633              
634             The folder in which the post lives, relative to the blog's base URL.
635              
636             =item fn
637              
638             The filename of the post, sans extension.
639              
640             =back
641              
642             =head3 Head template
643              
644             =over
645              
646             =item title
647              
648             This method can be overridden by a plugin.
649              
650             =cut
651              
652             sub interpolate {
653 0     0 1   my ($self, $template, $extra_data) = @_;
654            
655 0           my $done;
656 0 0         return $done if $done = $self->_check_plugins("interpolate", @_);
657              
658 0           for my $var (keys %$extra_data){
659 0 0         if($self->{require_namespace}) {
660 0           $template =~ s/\$blosxom::$var\b/$extra_data->{$var}/g;
661             }
662             else {
663 0           $template =~ s/\$(?:blosxom::)?$var\b/$extra_data->{$var}/g;
664             }
665             }
666              
667             # The blosxom docs say these are global vars, so let's mimic that.
668 0           for my $var (qw(blog_title blog_description blog_language url path_info flavour)) {
669             # You can set this option so that only $blosxom::foo variables are interpolated
670 0 0         if($self->{require_namespace}) {
671 0           $template =~ s/\$blosxom::$var\b/$self->{$var}/g;
672             }
673             else {
674 0           $template =~ s/\$(?:blosxom::)?$var\b/$self->{$var}/g;
675             }
676             }
677              
678             {
679 1     1   8 no strict 'vars';
  1         3  
  1         1306  
  0            
680             # Non-blosxom:: variables must be namespaced. I can't be bothered
681             # making it work with more than one :: in it just yet, sorry.
682 0           $template =~ s/\$(\w+::\w+)/"defined \$$1 ? \$$1 : '\$$1'"/gee;
  0            
683             }
684              
685 0           return $template;
686             }
687              
688             =head2 entry_data ($entry)
689              
690             Provided with the entry data, which is an arrayref with the entry filename,
691             relative to datadir, in the first slot and a hashref in the second. The hashref
692             will have at least a date entry, being a UNIX timestamp for the entry. See
693             the section on plugin entries.
694              
695             Returns a hashref containing the following keys:
696              
697             =over
698              
699             =item title
700              
701             Post title
702              
703             =item body
704              
705             The body of the post
706              
707             =item yr
708              
709             =item mo
710              
711             =item mo_num
712              
713             =item da
714              
715             =item dw
716              
717             =item hr
718              
719             =item min
720              
721             Timestamp of entry. mo = month name; dw = day name
722              
723             =item path
724              
725             The folder in which the post lives, relative to the blog's base URL.
726              
727             =item fn
728              
729             The filename of the post, sans extension.
730              
731             =back
732              
733             These should be returned such that it is true that
734              
735             $path . "/" . $fn . "." . $flavour eq $request_url
736              
737             i.e. these components together are what was originally asked for. (Note that
738             flavour is a variable available to templates but not returned by this method.)
739              
740             =cut
741              
742             sub entry_data {
743 0     0 1   my ($self, $entry) = @_;
744              
745 0           my $entry_data = {};
746              
747 0           my $fn = $entry->[0];
748              
749             {
750 0           open my $fh, "<", File::Spec->catfile($self->{datadir}, $fn);
  0            
751 0           my $title = <$fh>; chomp $title;
  0            
752 0           $entry_data->{title} = $title;
753              
754 0           $entry_data->{body} = join "", <$fh>;
755 0           close $fh;
756             }
757            
758             {
759 0           my @path = (File::Spec->splitpath($fn));
  0            
760 0           $fn = pop @path;
761 0           $fn =~ s/\.$self->{file_extension}$//;
762 0           $entry_data->{fn} = $fn;
763 0           $entry_data->{path} = File::Spec->catpath(@path);
764             }
765              
766             {
767 0           my $i = 1;
  0            
768 0           my %month2num = map {$_, $i++} qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
  0            
769 0           my $c_time = ctime($entry->[1]->{date});
770              
771 0           my($dw,$mo,$da,$hr,$min,$yr) = ( $c_time =~ /(\w{3}) +(\w{3}) +(\d{1,2}) +(\d{2}):(\d{2}):\d{2} +(\d{4})$/ );
772              
773 0           $da = sprintf("%02d", $da);
774 0           my $mo_num = $month2num{$mo};
775 0           $mo_num = sprintf("%02d", $mo_num);
776              
777 0           @{$entry_data}{qw(dw mo da yr mo_num hr min)} = ($dw, $mo, $da, $yr, $mo_num, $hr, $min);
  0            
778              
779             # Keep track of the latest date we find.
780 0 0         $self->{latest_entry_date} = 0 if !exists $self->{latest_entry_date};
781 0 0         $self->{latest_entry_date} = $entry->[1]->{date}
782             if $entry->[1]->{date} > $self->{latest_entry_date};
783             }
784              
785 0           return $entry_data;
786             }
787              
788             =head2 head_data ()
789              
790             Return the data you want to be available to the head template. The head is
791             attached to the top of the output I the entries have been run through,
792             so you have the data for all the entry data available to you in the arrayref
793             $self->{entries}.
794              
795             Example:
796              
797             my $self = shift;
798             my $data = {};
799             if(@{$self->{entries}} == 1) {
800             $data->{title} = $self->{entries}->[0]->{title}
801             }
802             return $data;
803              
804             =cut
805              
806             sub head_data {
807 0     0 1   +{};
808             }
809              
810             =head2 foot_data ()
811              
812             Return the data you want to be available in your foot template. This is attached
813             to the output after everything else, as you'd expect.
814              
815             =cut
816              
817             sub foot_data {
818 0     0 1   +{};
819             }
820              
821             ## PRIVATE FUNCTIONS
822             # Purposely not in the POD
823              
824             ## _load_plugins
825             # Trawl the plugins directory and look for plugins. Put them in the object hash.
826              
827             sub _load_plugins {
828 0     0     my $self = shift;
829              
830 0           my $dir = $self->{plugins_dir};
831 0 0         return unless $dir;
832              
833 0           opendir my($plugins), $dir;
834              
835             # blosxom docs say modules ending in _ will not be loaded.
836 0 0 0       for my $plugin (grep { /^\w+$/ && !/_$/ && -f File::Spec->join($dir, $_) }
  0            
837             sort readdir $plugins) {
838             # blosxom docs say you can order modules by prefixing numbers.
839 0           $plugin =~ s/^\d+//;
840              
841             # This will blow up if your package name is not the same as your file name.
842 0           require "$dir/$plugin";
843 0 0         if ($plugin->start()) {
844 0           $self->{active_plugins}->{$plugin} = 1;
845              
846 0   0       $self->{plugins_ordered} ||= [];
847 0           push @{$self->{plugins_ordered}}, $plugin;
  0            
848             }
849             }
850              
851 0           closedir $plugins;
852             }
853              
854             ## _load_templates
855             # Read the default templates from DATA. Later the plugins get an opportunity to
856             # override what happens when the real templates are read in, so we don't do that here.
857              
858             sub _load_templates {
859 0     0     my $self = shift;
860              
861 0           while () {
862 0 0         last if /^(__END__)?$/;
863 0           my($flavour, $comp, $txt) = /^(\S+)\s(\S+)\s(.*)$/;
864 0           $txt =~ s/\\n/\n/mg;
865 0           $self->{template}{$flavour}{$comp} = $txt;
866             }
867              
868             }
869              
870             ## _check_plugins
871             # Look for plugins that can do the first arg, and pass them the rest of the args.
872             # Return the first value returned by a plugin.
873              
874             sub _check_plugins {
875 0     0     my ($self, $method, @underscore) = @_;
876              
877 0 0         return unless $self->{plugins_ordered};
878 0 0         return if $self->{no_plugins};
879              
880 0           for my $plugin (@{$self->{plugins_ordered}}) {
  0            
881 0           local $self->{no_plugins} = 1;
882 0           my @return;
883 0 0         @return = $plugin->$method($self, @underscore) if $plugin->can($method);
884              
885 0 0         return @return if @return;
886             }
887             }
888              
889             =head1 USAGE
890              
891             =head2 Quick start
892              
893             To quick start, first you need to create a Blosxom object. You have to provide
894             three parameters to the C method:
895              
896             datadir
897             blog_title
898             blog_description
899              
900             The latter is likely to be dropped as a requirement soon.
901              
902             Then you need to find some way of collecting a path and a flavour from the user.
903             The original script used the URL provided by the web server. You provide these
904             to the C method.
905              
906             use Blog::Blosxom;
907             use CGI qw(standard);
908              
909             my $blog = Blog::Blosxom->new(
910             datadir => '/var/www/blosxom/entries',
911             blog_title => 'Descriptive blog title.',
912             blog_description => 'Descriptive blog description.',
913             );
914              
915             my $path = path_info() || param('path');
916             my ($flavour) = $path =~ s/(\.\w+)$// || param('flav');
917              
918             print header,
919             $blog->run($path, $flavour);
920              
921             The above is a complete CGI script that will run your blog. Note that C
922             is a CGI function. Don't print that if you're not using CGI!
923              
924             Now that we know how to run Blosxom we can look at how to make entries.
925              
926             =head2 Entries
927              
928             To create an entry, create a plaintext file in your C with the .txt
929             extension. The first line of this file is the title of the post and the rest
930             is the body.
931              
932             This post will be displayed if it is somewhere under the C<$path> you provided
933             to C, unless it is the 41st such file, because by default only 40 are
934             displayed at once.
935              
936             The C part of all this is configurable in C.
937              
938             =head2 Flavour
939              
940             The flavour that you provide determines which set of templates are used to
941             compose the output blog.
942              
943             You may be wondering about the fact that the blog entry ends with .txt, but in
944             the CGI script we have used the extension to determine the flavour. The file
945             extension is ignored when mapping the path to the file system, so your path
946             could feasibly match a single entry, which will of course be served on its own.
947              
948             The template to be chosen is a file whose file extension is the current flavour
949             and whose file name is the template in question. Have a look at the docs for the
950             C