| 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, '⇐'); | 
| 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; |