File Coverage

lib/WRT.pm
Criterion Covered Total %
statement 293 324 90.4
branch 65 102 63.7
condition 9 16 56.2
subroutine 36 38 94.7
pod 22 24 91.6
total 425 504 84.3


line stmt bran cond sub pod time code
1             =pod
2              
3             =head1 NAME
4              
5             WRT - WRiting Tool
6              
7             =for HTML <a href="https://travis-ci.org/brennen/wrt"><img src="https://travis-ci.org/brennen/wrt.svg?branch=master"></a>
8              
9             =head1 SYNOPSIS
10              
11             $ wrt display 2016 > 2016.html
12              
13             Or:
14              
15             $ wrt render
16              
17             Or:
18              
19             #!/usr/bin/env perl
20              
21             use WRT;
22             my $w = WRT->new(
23             entry_dir => 'archives',
24             url_root => '/',
25             # etc.
26             );
27             print $w->display(@ARGV);
28              
29             =head1 INSTALLING
30              
31             It's possible this may run on a Perl as old as 5.10.0, although in practice I
32             imagine that at least some of its dependencies have more recent requirements.
33             In practice, I know that it works under 5.20.2. It should work on any
34             reasonably modern Linux distribution, and may also be fine on MacOS or a BSD of
35             your choosing.
36              
37             $ perl Build.PL
38             $ ./Build installdeps
39             $ ./Build test
40             $ ./Build install
41              
42             =head1 DESCRIPTION
43              
44             This started life as C<display.pl>, a simple script to concatenate fragments of
45             handwritten HTML by date. It has since haphazardly accumulated several of the
46             usual weblog features (lightweight markup, feed generation, embedded Perl,
47             poetry tools, image galleries, and ill-advised dependencies), but the basic
48             idea hasn't changed that much.
49              
50             The C<wrt> utility now generates static HTML files, instead of expecting to
51             run as a CGI script. This is a better idea, for the most part.
52              
53             The C<WRT> module will work with FastCGI, if called from the appropriate
54             wrapper script, such as C<wrt-fcgi>.
55              
56             By default, entries are stored in a simple directory tree under C<entry_dir>.
57              
58             Like:
59              
60             archives/2001/1/1
61             archives/2001/1/1/sub_entry
62              
63             It is possible (although not as flexible as it ought to be) to redefine the
64             directory layout. More about this after a bit.
65              
66             An entry may be either a plain text file, or a directory containing several
67             files. If it's a directory, a file named "index" will be treated as the text
68             of the entry, and all other lower-case filenames without extensions will be
69             treated as sub-entries or documents within that entry, and displayed
70             accordingly. Links to certain other filetypes will be displayed as well.
71              
72             Directories may be nested to an arbitrary depth, although it's probably not a
73             good idea to go very deep with the current display logic.
74              
75             A PNG or JPEG file with a name like
76              
77             2001/1/1.icon.png
78             2001/1/1/index.icon.png
79             2001/1/1/whatever.icon.png
80             2001/1/1/whatever/index.icon.png
81              
82             will be treated as an icon for the appropriate entry file.
83              
84             =head2 MARKUP
85              
86             Entries may consist of hand-written HTML (to be passed along without further
87             interpretation), a supported form of lightweight markup, or some combination
88             thereof. Actually, an entry may consist of any darn thing you please, as long
89             as Perl will agree that it is text, but presumably you're going to be feeding
90             this to a browser.
91              
92             Special markup is indicated by a variety of HTML-like container tags.
93              
94             B<Embedded Perl> - evaluated and replaced by whatever value you return
95             (evaluated in a scalar context):
96              
97             <perl>my $dog = "Ralph."; return $dog;</perl>
98              
99             This code is evaluated before any other processing is done, so you can return
100             any other markup understood by the script and have it handled appropriately.
101              
102             B<Interpolated variables> - actually keys to the hash underlying the WRT
103             object, for the moment:
104              
105             <perl>$self->title("About Ralph, My Dog"); return '';</perl>
106              
107             <p>The title is <em>${title}</em>.</p>
108              
109             This will change.
110              
111             Embedded code and variables are intended for use in the F<template> file, where
112             it's handy to drop in titles or conditionalize aspects of a layout. You want to
113             be careful with this sort of thing - it's useful in small doses, but it's also
114             a maintainability nightmare waiting to happen. (WordPress, I am looking at
115             you.)
116              
117             B<Several forms of lightweight markup>:
118              
119             <markdown>John Gruber's Markdown, by way of
120             Text::Markdown</markdown>
121              
122             <textile>Dean Allen's Textile, via Brad Choate's
123             Text::Textile.</textile>
124              
125             <freeverse>An easy way to
126             get properly broken lines
127             plus -- en and em dashes ---
128             for poetry and such.</freeverse>
129              
130             B<And a couple of shortcuts>:
131              
132             <image>filename.ext
133             alt text, if any</image>
134              
135             <list>
136             one list item
137              
138             another list item
139             </list>
140              
141             As it stands, freeverse, image, and list are not particularly robust.
142              
143             =cut
144              
145             package WRT;
146              
147             our ($VERSION) = '3.5.0';
148              
149 3     3   181498 use strict;
  3         20  
  3         74  
150 3     3   12 use warnings;
  3         5  
  3         67  
151 3     3   12 no warnings 'uninitialized';
  3         3  
  3         79  
152              
153 3     3   12 use base 'WRT::MethodSpit';
  3         4  
  3         662  
154              
155 3     3   15 use Cwd;
  3         5  
  3         140  
156 3     3   919 use HTML::Entities;
  3         13675  
  3         188  
157 3     3   1188 use JSON;
  3         26967  
  3         21  
158 3     3   1562 use XML::Atom::SimpleFeed;
  3         35855  
  3         118  
159              
160 3     3   775 use WRT::Date;
  3         8  
  3         154  
161 3     3   771 use WRT::HTML qw(:all);
  3         9  
  3         612  
162 3     3   753 use WRT::Image qw(image_size);
  3         8  
  3         187  
163 3     3   959 use WRT::Markup qw(line_parse image_markup eval_perl);
  3         8  
  3         190  
164 3     3   644 use WRT::Renderer qw(render);
  3         9  
  3         153  
165 3     3   698 use WRT::Util qw(dir_list get_date);
  3         6  
  3         9930  
166              
167             =head1 CONFIGURATION
168              
169             =over
170              
171             =item options
172              
173             See F<example/wrt.json> for a sample configuration.
174              
175             =cut
176              
177             my %default = (
178             root_dir => '.', # dir for wrt repository
179             entry_dir => 'archives', # dir for entry files
180             publish_dir => 'public', # dir to publish site to
181             url_root => "$0?", # root URL for building links
182             image_url_root => '', # same for images
183             template_dir => 'templates', # dir for template files
184             template => 'default', # template to use
185             title => '',
186             title_prefix => '',
187             stylesheet_url => undef,
188             favicon_url => undef,
189             feed_alias => 'feed',
190             author => undef,
191             description => undef,
192             content => undef, # place to stash content for templates
193             embedded_perl => 1, # evaluate embedded <perl> tags?
194             default_entry => 'new',
195             license => 'public domain',
196              
197             # List of years for the menu:
198             year_list => [ reverse(1997..(get_date('year') + 1900)) ],
199              
200             # What gets considered an entry _path_:
201             entrypath_expr => qr/^ ([a-z0-9_\/-]+) $/x,
202              
203             # What gets considered a subentry file (slightly misleading terminology here):
204             subentry_expr => qr/^[0-9a-z_-]+(\.(tgz|zip|tar[.]gz|gz|txt))?$/,
205              
206             # We'll show links for these, but not display them inline:
207             binfile_expr => qr/[.](tgz|zip|tar[.]gz|gz|txt|pdf)$/,
208             );
209              
210             =item entry_map(\%map)
211              
212             Takes a hashref which will dispatch entries matching various regexen to
213             the appropriate output methods. The default looks something like this:
214              
215             nnnn/[nn/nn/]doc_name - a document within a day.
216             nnnn/nn/nn - a specific day.
217             nnnn/nn - a month.
218             nnnn - a year.
219             doc_name - a document in the root directory.
220              
221             You can re-map things to an arbitrary archive layout.
222              
223             Since the entry map is a hash, and handle() simply loops over its keys, there
224             is no guaranteed precedence of patterns. Be extremely careful that no entry
225             will match more than one pattern, or you will wind up with unexpected behavior.
226             A good way to ensure that this does not happen is to use patterns like:
227              
228             qr(
229             ^ # start of string
230             [0-9/]{4}/ # year
231             [0-9]{1,2}/ # month
232             [0-9]{1,2] # day
233             $ # end of string
234             )x
235              
236             ...always marking the start and end of the string explicitly.
237              
238             This may eventually be rewritten to use an array so that the order can be
239             explicitly specified.
240              
241             =cut
242              
243             $default{entry_map} = {
244             qr'^[0-9/]{5,11}[a-z_/]+$' => sub { entry_stamped (@_, 'index') },
245              
246             qr'^[0-9]{4}/[0-9]{1,2}/
247             [0-9]{1,2}$'x => sub { entry_stamped (@_, 'all' ) },
248              
249             qr'^[0-9]{4}/[0-9]{1,2}$' => sub { month (@_ ) },
250             qr'^[0-9]{4}$' => sub { year (@_ ) },
251             qr'^[a-z_]' => sub { entry_stamped (@_, 'all' ) },
252             };
253              
254             =item entry_descriptions(\%descriptions)
255              
256             Takes a hashref which contains a map of entry titles to entry descriptions.
257              
258             =cut
259              
260             # TODO: this has gotten more than a little silly.
261             $default{entry_descriptions} = {
262             new => 'newest entries',
263             all => 'all entries',
264             };
265             {
266             foreach my $yr ( @{ $default{year_list} } ) {
267             $default{entry_descriptions}{$yr} = "entries for $yr";
268             }
269             }
270              
271             # Set up some accessor methods:
272             __PACKAGE__->methodspit( keys %default );
273              
274             =back
275              
276             =head1 METHODS
277              
278             For no bigger than this thing is, it gets a little convoluted.
279              
280             =over
281              
282             =item new_from_file($config_file)
283              
284             Takes a filename to pull JSON config data out of, and
285             returns a new WRT instance with the parameters set in
286             that file.
287              
288             =cut
289              
290             sub new_from_file {
291 2     2 1 236 my ($config_file) = @_;
292              
293             # Grab configuration from wrt.json:
294 2         3 my $config_json;
295             {
296 2 50       3 open my $fh, '<', $config_file
  2         44  
297             or warn "Couldn't open configuration file: $config_file: $!\n";
298             # line separator:
299 2         8 local $/ = undef;
300 2         34 $config_json = <$fh>;
301 2         15 close $fh;
302             }
303              
304 2         39 my $JSON = JSON->new->utf8->pretty;
305 2         11 $JSON->convert_blessed(1);
306              
307 2         24 my $config_hashref = $JSON->decode($config_json);
308              
309             # decode() returns (I think) a hashref; this needs to be dereferenced:
310 2         4 return WRT->new(%{ $config_hashref });
  2         14  
311             }
312              
313             =item new(%params)
314              
315             Get a new WRT object with the specified parameters set.
316              
317             =cut
318              
319             sub new {
320 3     3 1 106 my $class = shift;
321 3         13 my %params = @_;
322              
323 3         7 my $self = \%default;
324 3         6 bless $self, $class;
325              
326 3         28 $self->configure(%params);
327              
328 3         23 return $self;
329             }
330              
331             =item display($entry1, $entry2, ...)
332              
333             Return a string containing the given entries, which are in the form of
334             date/entry strings. If no parameters are given, default to default_entry().
335              
336             display() expands aliases ("new" and "all", for example) as necessary, collects
337             output from handle($entry), and wraps the whole thing in a template file.
338              
339             =cut
340              
341             sub display {
342 15     15 1 257 my $self = shift;
343 15         33 my (@options) = @_;
344              
345 15   33     27 $options[0] ||= $self->default_entry;
346 15         24 $self->title(join ' ', map { encode_entities($_) } @options); # title for head/foot
  15         35  
347              
348             # Expand on any aliases:
349 15         20 @options = map { $self->expand_option($_) } @options;
  15         34  
350              
351 15         38 $self->content(undef);
352 15         11 my $output;
353 15         21 for my $option (@options) {
354 15 100       24 return $self->feed_print() if $option eq $self->feed_alias;
355 14         22 $output .= $self->handle($option);
356             }
357 14         44 $self->content($output); # ${content} may now be used in the template below...
358              
359             # Wrap entries in template:
360 14         32 my $rendered_page;
361 14 50       25 if ($self->{overlay}) {
362 0         0 $rendered_page .= $self->{overlay};
363             } else {
364 14         32 $rendered_page .= $self->fragment_slurp($self->template_dir . '/' . $self->template);
365             }
366              
367 14         51 return $rendered_page;
368             }
369              
370             =item handle($entry)
371              
372             Return the text of an individual entry.
373              
374             =begin digression
375              
376             =item A digression about each()
377              
378             I once spent a lot of time chasing down a bug caused by a while loop in this
379             method. Specifically, I was using while to iterate over the entry_map hash.
380             Since C<$self->entry_map> returns a reference to the same hash each time, every
381             other request was finding C<each()> mid-way through iterating over this hash.
382              
383             I initially solved this by copying the hash into a local one called C<%map>
384             every time C<handle()> was called. I could also have called C<keys> or
385             C<values> on the anonymous hash, as these reset C<each()>.
386              
387             Presently I'm not using each() or an explicit loop, so this probably doesn't
388             make a whole lot of sense in the context of the existing code.
389              
390             =end digression
391              
392             =cut
393              
394             sub handle {
395 14     14 1 16 my $self = shift;
396 14         17 my ($entry) = @_;
397              
398             # Hashref:
399 14         30 my $map = $self->entry_map;
400              
401             # Find the first pattern in entry_map that matches this entry...
402 14         13 my ($pattern) = grep { $entry =~ $_ } keys %{ $map };
  70         1016  
  14         38  
403              
404 14 50       34 return unless defined $pattern;
405              
406             # ...and use the corresponding coderef to handle the entry:
407 14         41 return $map->{$pattern}->($self, $entry);
408             }
409              
410             =item expand_option($option)
411              
412             Expands/converts 'all' and 'new' to appropriate values.
413              
414             =cut
415              
416             sub expand_option {
417 15     15 1 23 my ($self, $option) = @_;
418              
419             # Take care of trailing slashes:
420 15 50       36 chop $option if $option =~ m{/$};
421              
422 15 100       40 if ($option eq 'all') {
    100          
    50          
423 1         7 return dir_list($self->entry_dir, 'high_to_low', qr/^[0-9]{1,4}$/);
424             } elsif ($option eq 'new') {
425 1         3 return $self->recent_month();
426             } elsif ($option eq 'fulltext') {
427 0         0 return $self->fulltext();
428             } else {
429 13         31 return $option;
430             }
431             }
432              
433              
434             =item recent_month()
435              
436             Tries to find the most recent month in the archive.
437              
438             If a year file is text, returns that instead.
439              
440             =cut
441              
442             sub recent_month {
443 5     5 1 7 my $self = shift;
444 5         11 my ($dir) = $self->entry_dir;
445              
446 5         15 my ($mon, $year) = get_date('mon', 'year');
447              
448 5         8 $mon++;
449 5         6 $year += 1900;
450              
451 5 50       50 if (-e "$dir/$year/$mon") {
452 0         0 return "$year/$mon";
453             } else {
454 5         29 my @year_files = dir_list($dir, 'high_to_low', qr/^[0-9]{1,4}$/);
455              
456 5 50       52 return $year_files[0] if -f "$dir/$year_files[0]";
457              
458 5         29 my @month_files = dir_list(
459             "$dir/$year_files[0]", 'high_to_low', qr/^[0-9]{1,2}$/
460             );
461              
462 5         32 return "$year_files[0]/$month_files[0]";
463             }
464             }
465              
466             =item fulltext
467              
468             The full text of all entries, in order.
469              
470             =cut
471              
472             sub fulltext {
473 0     0 1 0 my $self = shift;
474              
475 0         0 my @individual_entries;
476              
477 0         0 my @years = dir_list($self->entry_dir, 'low_to_high', qr/^[0-9]{1,4}$/);
478 0         0 foreach my $year (@years) {
479 0         0 my @months = dir_list($self->entry_dir . '/' . $year, 'low_to_high', qr/^[0-9]+$/);
480 0         0 foreach my $month (@months) {
481 0         0 my @days = dir_list($self->entry_dir . '/' . $year . '/' . $month, 'low_to_high', qr/^[0-9]+$/);
482 0         0 foreach my $day (@days) {
483 0         0 push @individual_entries, "$year/$month/$day";
484             }
485             }
486             }
487              
488 0         0 return @individual_entries;
489             }
490              
491             =item link_bar(@extra_links)
492              
493             Returns a little context-sensitive navigation bar.
494              
495             =cut
496              
497             sub link_bar {
498 14     14 1 21 my $self = shift;
499 14         16 my (@extra_links) = @_;
500              
501 14         31 my $title = $self->title;
502              
503 14         14 my $output;
504              
505 14         14 my (%description) = %{ $self->entry_descriptions() };
  14         25  
506              
507 14         21 my @years = @{ $self->year_list };
  14         22  
508              
509             # This makes the short list of years context sensitive:
510              
511 14 100       51 if ( my ($title_year) = $title =~ m/^([0-9]{4})/ ) {
512             # We have a match.
513              
514 5 50       18 if ($title_year == $years[0] ) { $title_year--; }
  0 50       0  
515 0         0 elsif ($title_year == $years[-1]) { $title_year++; }
516              
517 5 50       7 if (grep { $title_year eq $_ } @years) {
  105         104  
518 5         7 my $prev = $title_year - 1;
519 5         5 my $next = $title_year + 1;
520 5         6 @years = grep { m/^($prev|$title_year|$next)$/ } @years;
  105         244  
521             }
522             } else {
523 9         46 @years = @years[0..2];
524             }
525              
526 14         35 my @linklist = ( qw(new all), @years, @extra_links );
527              
528 14         22 foreach my $link (@linklist) {
529 70         62 my $link_title;
530 70 100       90 if (exists $description{$link}) {
531 28         50 $link_title = $description{$link};
532             } else {
533 42         37 $link_title = 'entries for ' . $link;
534             }
535              
536 70 100       81 if ($title ne $link) {
537              
538 67         95 my $href = $self->url_root . $link . '/';
539 67 100       95 if ($link eq 'new') {
540 13         17 $href = $self->url_root;
541             }
542              
543 67         166 $output .= a({href => $href, title => $link_title}, $link) . "\n";
544              
545             } else {
546 3         12 $output .= qq{<strong><span title="$link_title">$link</span></strong>\n};
547             }
548             }
549              
550 14         114 return $output;
551             }
552              
553             =item month_before($this_month)
554              
555             Return the month before the given month in the archive.
556              
557             Very naive; there has got to be a smarter way.
558              
559             =cut
560              
561             { my %cache; # cheap memoization
562              
563             sub month_before {
564 2     2 1 3 my $self = shift;
565 2         4 my ($this_month) = @_;
566              
567 2 50       7 if (exists $cache{$this_month}) {
568 0         0 return $cache{$this_month};
569             }
570              
571 2         13 my ($year, $month) = $this_month =~
572             m/^ # start of string
573             ([0-9]{4}) # 4 digit year
574             \/ #
575             ([0-9]{1,2}) # 2 digit month
576             /x;
577              
578 2 50       6 if ($month == 1) {
579 2         4 $month = 12; $year--;
  2         6  
580             } else {
581 0         0 $month--;
582             }
583              
584 2         7 until (-e $self->local_path("$year/$month")) {
585              
586 2 50       4 if (! -d $self->local_path($year) ) {
587             # Give up easily, wrapping to newest month.
588 2         6 return $self->recent_month;
589             }
590              
591             # handle January:
592 0 0       0 if ($month == 1) {
593 0         0 $month = 12; $year--;
  0         0  
594 0         0 next;
595             }
596 0         0 $month--;
597             }
598              
599 0         0 return $cache{$this_month} = "$year/$month";
600             }
601             }
602              
603             =item year($year)
604              
605             List out the updates for a year.
606              
607             =cut
608              
609             sub year {
610 2     2 1 6 my $self = shift;
611 2         4 my ($year) = @_;
612              
613 2         8 my ($year_file, $year_url) = $self->root_locations($year);
614              
615             # Year is a text file:
616 2 50       26 return $self->entry_wrapped($year) if -f $year_file;
617              
618             # If it's not a directory, we can't do anything. Bail out:
619 2 50       30 return p('No such year.') if (! -d $year_file);
620              
621 2         2 my $result;
622              
623             # Handle year directories with index files.
624 2 50       22 $result .= $self->entry($year)
625             if -f "$year_file/index";
626              
627 2         8 my $header_text = $self->icon_markup($year, $year);
628 2   50     14 $header_text ||= q{};
629              
630 2         11 $result .= heading("${header_text}${year}", 3);
631              
632 2         26 my @months = dir_list($year_file, 'high_to_low', qr/^[0-9]{1,2}$/);
633              
634 2         3 my $year_text;
635 2         5 my $count = 0; # explicitly defined for later printing.
636              
637 2         5 foreach my $month (@months) {
638 2         13 my @entries = dir_list(
639             "$year_file/$month", 'low_to_high', qr/^[0-9]{1,2}$/
640             );
641 2         5 $count += @entries;
642              
643 2         3 my $month_text;
644 2         4 foreach my $entry (@entries) {
645 4         17 $month_text .= a({href => "$year_url/$month/$entry/"}, $entry) . "\n";
646             }
647              
648 2         9 $month_text = small("( $month_text )");
649              
650 2         12 my $link = a({href => "$year_url/$month/"}, month_name($month));
651              
652 2         13 $year_text .= table_row(
653             table_cell({class => 'datelink'}, $link),
654             table_cell({class => 'datelink'}, $month_text)
655             ) . "\n\n";
656             }
657              
658 2 50       5 if ($count > 1) {
    0          
    0          
659 2         6 $year_text .= table_row(
660             table_cell(scalar(@months) . ' months'),
661             table_cell("$count entries")
662             );
663             }
664 0         0 elsif ($count == 0) { $year_text .= table_row(table_cell('No entries')); }
665 0         0 elsif ($count == 1) { $year_text .= table_row(table_cell("$count entry")); }
666              
667 2         6 $result .= "\n\n" . table($year_text) . "\n";
668              
669 2         5 return entry_markup($result);
670             }
671              
672             =item month($month)
673              
674             Prints the entries in a given month (nnnn/nn).
675              
676             =cut
677              
678             sub month {
679 2     2 1 4 my $self = shift;
680 2         4 my ($month) = @_;
681              
682 2         6 my ($month_file, $month_url) = $self->root_locations($month);
683              
684 2         4 my $result;
685              
686             # If a directory exists for $month, use dir_list to slurp
687             # the entry files it contains into @entry_files, sorted
688             # numerically. Then send each entry to entry_markup().
689 2 50       27 if (-d $month_file) {
    0          
690              
691 2 50       26 $result .= $self->entry($month)
692             if -f "$month_file/index";
693              
694 2         14 my @entry_files = dir_list($month_file, 'high_to_low', qr/^[0-9]{1,2}$/);
695              
696 2         8 foreach my $entry_file (@entry_files) {
697 4         18 $result .= $self->entry_stamped("$month/$entry_file");
698             }
699              
700             } elsif (-f $month_file) {
701 0         0 $result .= $self->entry($month);
702             }
703              
704 2         6 my %link_params = (
705             href => $self->url_root . $self->month_before($month) . '/',
706             title => 'previous month'
707             );
708 2         6 my $prev_link = a(\%link_params, '&#8656;');
709              
710 2         8 $result .= div(
711             { class => 'entry' },
712             nav(p( {class => 'navigation'}, $prev_link )) . "\n\n"
713             );
714              
715 2         14 return $result;
716             }
717              
718             =item entry_wrapped
719              
720             Wraps entry() in entry_markup.
721              
722             =cut
723              
724             sub entry_wrapped {
725 0     0 1 0 my $self = shift;
726 0         0 my ($entry, $level) = @_;
727              
728 0         0 return entry_markup($self->entry($entry, $level));
729             }
730              
731             =item entry_stamped
732              
733             Wraps entry() + a datestamp in entry_markup()
734              
735             =cut
736              
737             sub entry_stamped {
738 14     14 1 18 my $self = shift;
739 14         27 my ($entry, $level) = @_;
740              
741 14         27 return entry_markup(
742             $self->entry($entry, $level)
743             . $self->datestamp($entry)
744             );
745             }
746              
747             =item entry_topic_list
748              
749             Get a list of topics (by tag-* files) for the entry. This hardcodes a
750             p1k3-specific thing, and is dumb.
751              
752             =cut
753              
754             sub entry_topic_list {
755 17     17 1 19 my $self = shift;
756 17         26 my ($entry) = @_;
757              
758             # Location of entry on local filesystem, and its URL:
759 17         49 my ($entry_loc, $entry_url) = $self->root_locations($entry);
760              
761 17         27 my @tag_files;
762              
763             # If it's a directory, look for some tag property files:
764 17 100       237 if (-d $entry_loc) {
765 13         44 @tag_files = dir_list($entry_loc, 'alpha', '^tag-.*[.]prop$');
766             }
767              
768 17 100       99 return '' unless @tag_files;
769              
770             return join ', ', map {
771 4         6 s/^tag-(.*)[.]prop$/$1/;
  4         21  
772 4         17 a($_, { href => '/topics/' . $_ })
773             } @tag_files;
774             }
775              
776             =item entry($entry)
777              
778             Returns the contents of a given entry. Calls dir_list
779             and icon_markup. Recursively calls itself.
780              
781             =cut
782              
783             sub entry {
784 22     22 1 24 my $self = shift;
785 22         29 my ($entry, $level) = @_;
786 22   100     59 $level ||= 'index';
787              
788             # Location of entry on local filesystem, and its URL:
789 22         41 my ($entry_loc, $entry_url) = $self->root_locations($entry);
790              
791 22         26 my $result;
792              
793             # Display an icon, if we have one:
794 22 100       36 if ( my $ico_markup = $self->icon_markup($entry) ) {
795 8         37 $result .= heading($ico_markup, 2) . "\n\n";
796             }
797              
798             # For text files:
799 22 100       149 if (-f $entry_loc) {
800 7         28 return $result . $self->fragment_slurp($entry_loc);
801             }
802              
803 15 50       73 return $result if ! -d $entry_loc;
804              
805             # Print index as head, if extant and a normal file:
806 15 100       121 if (-f "$entry_loc/index") {
807 13         39 $result .= $self->fragment_slurp("$entry_loc/index");
808             }
809              
810             # Followed by any sub-entries:
811 15         43 my @sub_entries = $self->get_sub_entries($entry_loc);
812              
813 15 100       38 if (@sub_entries >= 1) {
814             # If the wrt-noexpand property is present, then don't expand
815             # sub-entries. A hack.
816 9 100 66     71 if ($level eq 'index' || -f "$entry_loc/wrt-noexpand.prop") {
    50          
817             # Icons or text links:
818 5         18 $result .= $self->list_contents($entry, @sub_entries);
819             }
820             elsif ($level eq 'all') {
821             # Everything in the directory:
822 4         8 foreach my $se (@sub_entries) {
823 6 50       17 next if ($se =~ $self->binfile_expr);
824 6         22 $result .= p({class => 'centerpiece'}, '+')
825             . $self->entry("$entry/$se");
826             }
827             }
828             }
829              
830 15         81 return $result;
831             }
832              
833             sub get_sub_entries {
834 15     15 0 20 my $self = shift;
835 15         23 my ($entry_loc) = @_;
836              
837 15         39 my %ignore = ('index' => 1);
838              
839 15         34 return grep { ! $ignore{$_} }
  24         71  
840             dir_list($entry_loc, 'alpha', $self->subentry_expr);
841             }
842              
843             sub list_contents {
844 5     5 0 8 my $self = shift;
845 5         8 my ($entry) = shift;
846 5         9 my (@entries) = @_;
847              
848 5         5 my $contents;
849 5         12 foreach my $se (@entries) {
850 5         15 my $linktext = $self->icon_markup("$entry/$se", $se);
851 5   66     24 $linktext ||= $se;
852              
853 5         77 $contents .= q{ }
854             . a({ href => $self->url_root . "$entry/$se",
855             title => $se },
856             $linktext);
857             }
858              
859 5         12 return p( em('more:') . " $contents" ) . "\n";
860             }
861              
862             =item icon_markup
863              
864             Check if an icon exists for a given entry if so, return markup to include it.
865             Icons are PNG or JPEG image files following a specific naming convention:
866              
867             index.icon.[png|jp(e)g] for directories
868             [filename].icon.[png|jp(e)g] for flat text files
869              
870             Calls image_size, uses filename to determine type.
871              
872             =cut
873              
874             { my %cache;
875             sub icon_markup {
876 29     29 1 30 my $self = shift;
877 29         35 my ($entry, $alt) = @_;
878              
879 29 100       70 if ($cache{$entry . $alt}) {
880 6         17 return $cache{$entry.$alt};
881             }
882              
883 23         44 my ($entry_loc, $entry_url) = $self->root_locations($entry);
884              
885 23         27 my ($icon_loc, $icon_url);
886              
887 23 100       275 if (-f $entry_loc) {
    50          
888 8         17 $icon_loc = "$entry_loc.icon";
889 8         11 $icon_url = "$entry_url.icon";
890             }
891             elsif (-d $entry_loc) {
892 15         26 $icon_loc = "$entry_loc/index.icon";
893 15         24 $icon_url = "$entry_url/index.icon";
894             }
895              
896             # First suffix found will be used:
897 23         40 my (@suffixes) = qw(png jpg gif jpeg);
898 23         24 my $suffix;
899 23         30 for (@suffixes) {
900 80 100       396 if (-e "$icon_loc.$_") {
901 4         6 $suffix = $_;
902 4         6 last;
903             }
904             }
905              
906             # fail unless there's a file with one of the above suffixes
907 23 100       69 return 0 unless $suffix;
908              
909             # call image_size to slurp width & height from the image file
910 4         22 my ($width, $height) = image_size("$icon_loc.$suffix");
911              
912 4         9752 return $cache{$entry . $alt} =
913             qq{<img src="$icon_url.$suffix"\n width="$width" }
914             . qq{height="$height"\n alt="$alt" />};
915             }
916             }
917              
918             =item datestamp
919              
920             Returns a nice html datestamp / breadcrumbs for a given entry.
921              
922             =cut
923              
924             sub datestamp {
925 17     17 1 26 my $self = shift;
926 17         26 my ($entry) = @_;
927              
928 17         17 my ($stamp);
929              
930             # Chop up by directory separator.
931 17         54 my @pieces = split '/', $entry;
932              
933 17         18 my (@fragment_stack);
934 17         44 my (@fragment_stamps) = (
935             a({ href => $self->url_root }, $self->title_prefix),
936             );
937              
938 17         39 foreach my $fragment (@pieces) {
939 43         66 push @fragment_stack, $fragment;
940 43         91 push @fragment_stamps,
941             a({ href => $self->url_root . (join '/', @fragment_stack) . '/',
942             title => $fragment }, $fragment);
943             }
944              
945 17         40 $stamp = "\n"
946             . $self->entry_topic_list($entry)
947             . " :: "
948             . join(" /\n", @fragment_stamps)
949             . "\n";
950              
951 17         69 return p({class => 'datelink'}, $stamp);
952             }
953              
954              
955             =item fragment_slurp
956              
957             Read a text fragment, call line_parse() and eval_perl() to take care of funky
958             markup and interpreting embedded code, and then return it as a string. Takes
959             one parameter, the name of the file, and returns '' if it's not an extant text
960             file.
961              
962             This might be the place to implement an in-memory cache for FastCGI or mod_perl
963             environments. The trick is that the results for certain files shouldn't be
964             cached because they contain embedded code.
965              
966             =cut
967              
968             sub fragment_slurp {
969 34     34 1 41 my $self = shift;
970              
971 34         48 my ($file) = @_;
972              
973 34         33 my $everything;
974              
975 34 50       699 open my $fh, '<', $file
976             or warn "Couldn't open $file: $!\n";
977              
978             {
979             # line separator:
980 34         53 local $/ = undef;
  34         120  
981 34         429 $everything = <$fh>;
982             }
983              
984 34 50       176 close $fh or warn "Couldn't close: $!";
985              
986 34 50       100 return $self->line_parse(
987             # handle embedded perl first
988             ($self->embedded_perl ? $self->eval_perl($everything) : $everything),
989             $file # some context to work with
990             );
991             }
992              
993              
994             =item month_name
995              
996             Turn numeric dates into English.
997              
998             =cut
999              
1000             sub month_name {
1001 2     2 1 5 my ($number) = @_;
1002              
1003             # "Null" is here so that $month_name[1] corresponds to January, etc.
1004 2         17 my @months = qw(Null January February March April May June
1005             July August September October November December);
1006              
1007 2         8 return $months[$number];
1008             }
1009              
1010             =item root_locations($file)
1011              
1012             Given a file/entry, return the appropriate concatenations with
1013             entry_dir and url_root.
1014              
1015             =cut
1016              
1017             sub root_locations {
1018             return (
1019 67     67 1 118 $_[0]->local_path($_[1]),
1020             $_[0]->url_root . $_[1]
1021             );
1022             }
1023              
1024             =item local_path
1025              
1026             Return an absolute path for a given file. Called by root_locations.
1027              
1028             Arguably this is stupid and inefficient.
1029              
1030             =cut
1031              
1032             sub local_path {
1033 71     71 1 135 return $_[0]->entry_dir . '/' . $_[1];
1034             }
1035              
1036             =item feed_print
1037              
1038             Return an Atom feed of entries for a month. Defaults to the most
1039             recent month in the archive.
1040              
1041             Called from handle(), requires XML::Atom::SimpleFeed.
1042              
1043             =cut
1044              
1045             sub feed_print {
1046 1     1 1 2 my $self = shift;
1047 1         2 my ($month) = @_;
1048 1   33     9 $month ||= $self->recent_month();
1049              
1050 1         4 my $feed_url = $self->url_root . $self->feed_alias;
1051              
1052 1         4 my ($month_file, $month_url) = $self->root_locations($month);
1053              
1054 1         4 my $feed = XML::Atom::SimpleFeed->new(
1055             title => $self->title_prefix . '::' . $self->title,
1056             link => $self->url_root,
1057             link => { rel => 'self', href => $feed_url, },
1058             icon => $self->favicon_url,
1059             author => $self->author,
1060             id => $self->url_root,
1061             generator => 'WRT.pm / XML::Atom::SimpleFeed',
1062             updated => WRT::Date::iso_date(WRT::Date::get_mtime($month_file)),
1063             );
1064              
1065 1         1440 my @entry_files;
1066              
1067 1 50       14 if (-d $month_file) {
1068 1         7 @entry_files = dir_list($month_file, 'high_to_low', qr/^[0-9]{1,2}$/);
1069             } else {
1070 0         0 return 0;
1071             }
1072              
1073 1         3 foreach my $entry_file (@entry_files) {
1074 2         531 my $entry = "$month/$entry_file";
1075 2         4 my $entry_url = $month_url . "/$entry_file";
1076 2         3 my $title = $entry;
1077 2         5 my $content = $self->entry($entry) . "\n" . $self->datestamp($entry);
1078              
1079             # try to pull out a header:
1080 2         15 my ($extracted_title) = $content =~ m{<h1>(.*?)</h1>}s;
1081 2         7 my (@subtitles) = $content =~ m{<h2>(.*?)</h2>}sg;
1082              
1083 2 50       6 if ($extracted_title) {
1084 2         3 $title = $extracted_title;
1085 2 50       3 if (@subtitles) {
1086 0         0 $title .= ' - ' . join ' - ', @subtitles;
1087             }
1088             }
1089              
1090             $feed->add_entry(
1091 2         9 title => $title,
1092             link => $entry_url,
1093             id => $entry_url,
1094             content => $content,
1095             updated => WRT::Date::iso_date(WRT::Date::get_mtime("$month_file/$entry_file")),
1096             );
1097             }
1098              
1099             # return "Content-type: application/atom+xml\n\n" . $feed->as_string;
1100 1         516 return $feed->as_string;
1101             }
1102              
1103              
1104             =back
1105              
1106             =head1 SEE ALSO
1107              
1108             walawiki.org, Blosxom, rassmalog, Text::Textile, XML::Atom::SimpleFeed,
1109             Image::Size, CGI::Fast, and about a gazillion static site generators.
1110              
1111             =head1 AUTHOR
1112              
1113             Copyright 2001-2017 Brennen Bearnes
1114              
1115             =head1 LICENSE
1116              
1117             wrt is free software; you can redistribute it and/or modify
1118             it under the terms of the GNU General Public License as published by
1119             the Free Software Foundation; either version 2 of the License, or
1120             (at your option) any later version.
1121              
1122             This program is distributed in the hope that it will be useful,
1123             but WITHOUT ANY WARRANTY; without even the implied warranty of
1124             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1125             GNU General Public License for more details.
1126              
1127             You should have received a copy of the GNU General Public License
1128             along with this program. If not, see <http://www.gnu.org/licenses/>.
1129              
1130             =cut
1131              
1132             1;