Almost-minimal filesystem based blog.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

946 lines
21 KiB

17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
16 years ago
16 years ago
16 years ago
16 years ago
16 years ago
17 years ago
16 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
  1. package Display;
  2. our ($VERSION) = '$Revision$' =~ m{ \$Revision: \s+ (\S+) }x;
  3. # $Author$
  4. # $Date$
  5. # $Id$
  6. use strict;
  7. use warnings;
  8. no warnings 'uninitialized';
  9. use base 'MethodSpit';
  10. use XML::Atom::SimpleFeed;
  11. use Wala;
  12. use Display::HTML qw(:highlevel);
  13. use Display::Markup qw(line_parse image_markup);
  14. use Display::Image qw(image_size);
  15. =head1 CONFIGURATION
  16. =over
  17. =item default values
  18. =cut
  19. my %default = (
  20. root_dir => 'archives', # root dir for archived files
  21. url_root => "$0?", # root URL for building links
  22. image_url_root => '', # same for images
  23. header => 'header',
  24. footer => 'footer',
  25. title => '',
  26. stylesheet_url => undef,
  27. favicon_url => undef,
  28. feed_alias => 'feed',
  29. author => undef,
  30. description => undef,
  31. license => undef,
  32. http_header => 1,
  33. default_entry => 'new',
  34. # What gets considered an entry file:
  35. entryfile_expr => qr/^[a-z_]+(\.(tgz|zip|tar[.]gz|gz|txt))?$/,
  36. # We'll show links for these, but not display them inline:
  37. binfile_expr => qr/[.](tgz|zip|tar[.]gz|gz|txt|pdf)$/,
  38. wala => Wala->new(),
  39. );
  40. =item entry_map(\%map)
  41. Takes a hashref which will dispatch entries matching various regexen to
  42. the appropriate output methods. The default looks something like this:
  43. nnnn/[nn/nn/]doc_name - a document within a day.
  44. nnnn/nn/nn - a specific day.
  45. nnnn/nn - a month.
  46. nnnn - a year.
  47. doc_name - a document in the root directory.
  48. You can re-map things to an arbitrary archive layout.
  49. Since the entry map is a hash, and handle() simply loops over its keys, there
  50. is no guaranteed precedence of patterns. Be extremely careful that no entry
  51. will match more than one pattern, or you will wind up with unexpected behavior.
  52. A good way to ensure that this does not happen is to use patterns like:
  53. qr(
  54. ^ # start of string
  55. [0-9/]{4}/ # year
  56. [0-9]{1,2}/ # month
  57. [0-9]{1,2] # day
  58. $ # end of string
  59. )x
  60. ...always marking the start and end of the string explicitly.
  61. =cut
  62. $default{entry_map} = {
  63. qr'^[0-9/]{5,11}[a-z_/]+$' => sub { entry_stamped (@_ ) },
  64. qr'^[0-9]{4}/[0-9]{1,2}/
  65. [0-9]{1,2}$'x => sub { entry_stamped (@_, 'all') },
  66. qr'^[0-9]{4}/[0-9]{1,2}$' => sub { month (@_ ) },
  67. qr'^[0-9]{4}$' => sub { year (@_ ) },
  68. qr'^[a-z_]' => sub { entry_wrapped (@_, 'all') },
  69. };
  70. # Set up some accessor methods:
  71. __PACKAGE__->methodspit( keys %default );
  72. =back
  73. =head1 METHODS
  74. For no bigger than this thing is, it gets a little convoluted.
  75. =over
  76. =item new()
  77. =cut
  78. sub new {
  79. my $class = shift;
  80. my %params = @_;
  81. my $self = \%default;
  82. bless $self, $class;
  83. $self->configure(%params);
  84. return $self;
  85. }
  86. =item configure(param => 'value')
  87. Set specified parameters.
  88. =cut
  89. sub configure {
  90. my $self = shift;
  91. my %params = @_;
  92. for my $p (keys %params) {
  93. $self->{$p} = $params{$p};
  94. }
  95. return;
  96. }
  97. =item walaconf(%options)
  98. Set parameters for Wala.pm.
  99. =cut
  100. sub walaconf {
  101. my $self = shift;
  102. $self->wala->conf(@_);
  103. return;
  104. }
  105. =item display($entry1, $entry2, ...)
  106. Return a string containing the given entries, which can be in the form of CGI
  107. query objects or date/entry strings. If no parameters are given, default to
  108. default_entry().
  109. display() expands aliases ("new" and "all") and CGI query objects as necessary,
  110. collects input from handle($entry), and wraps the whole thing in header and
  111. footer files.
  112. =cut
  113. sub display {
  114. my $self = shift;
  115. my (@options) = @_;
  116. # Get parameters from any CGI queries, make sure we have at least the
  117. # default, and expand on any aliases:
  118. @options = map { expand_query($_) } @options;
  119. $options[0] ||= $self->default_entry;
  120. $self->title(join ' ', @options); # title for head/foot
  121. @options = map { $self->expand_option($_) } @options;
  122. my $output;
  123. for my $option (@options) {
  124. return $self->feed_print() if $option eq $self->feed_alias;
  125. $output .= $self->handle($option);
  126. }
  127. # Wrap entries in header/footer:
  128. my $header;
  129. $header .= "Content-Type: text/html\n\n"
  130. if $self->http_header;
  131. $header .= $self->fragment_slurp($self->header);
  132. return $header
  133. . $output
  134. . $self->fragment_slurp($self->footer);
  135. }
  136. =item handle($entry)
  137. Return the text of an individual entry.
  138. =cut
  139. # A digression about each():
  140. # I just spent a lot of time chasing down a bug caused by the while loop
  141. # below. Specifically, since $self->entry_map returns a reference to the
  142. # same hash each time, every other request was finding each() mid-way
  143. # through iterating over this hash.
  144. #
  145. # I solved this by copying this hash into a local one called %map every
  146. # time handle() is called. Another approach would be to call keys() or
  147. # values on the anonymous hash referenced by $self->entry_map, which
  148. # apparently resets each().
  149. sub handle {
  150. my $self = shift;
  151. my ($option) = @_;
  152. # Dispatch entries to output routines:
  153. my $output;
  154. my %map = %{ $self->entry_map };
  155. while ( my ($pattern, $dispatch) = each %map ) {
  156. if ($option =~ $pattern) {
  157. $output .= $dispatch->($self, $option);
  158. last;
  159. }
  160. }
  161. return $output;
  162. }
  163. =item expand_query
  164. Expands a CGI query object (for example, one passed in from CGI::Fast) to an
  165. appropriate list of parameters.
  166. =cut
  167. sub expand_query {
  168. my ($option) = shift;
  169. if ( (ref $option eq 'CGI::Fast') or (ref $option eq 'CGI') ) {
  170. return $option->param('keywords');
  171. } else {
  172. return $option;
  173. }
  174. }
  175. =item expand_option
  176. Expands/converts 'all' and 'new' to appropriate values.
  177. =cut
  178. sub expand_option {
  179. my ($self, $option) = @_;
  180. # Take care of trailing slashes:
  181. chop $option if substr($option, -1, 1) eq q{/};
  182. if ($option eq 'all') {
  183. return dir_list($self->root_dir, 'high_to_low', qr/^[0-9]{1,4}$/);
  184. } elsif ($option eq 'new') {
  185. return $self->recent_month;
  186. } else {
  187. return $option;
  188. }
  189. }
  190. =item recent_month
  191. Tries to find the most recent month in the archive.
  192. If a year file is text, returns that instead.
  193. =cut
  194. sub recent_month {
  195. my $self = shift;
  196. my ($dir) = $self->root_dir;
  197. my ($mon, $year) = get_date('mon', 'year');
  198. $mon++;
  199. $year += 1900;
  200. if (-e "$dir/$year/$mon") {
  201. return "$year/$mon";
  202. }
  203. else {
  204. my @year_files = dir_list($dir, 'high_to_low', qr/^[0-9]{1,4}$/);
  205. return $year_files[0] if -T "$dir/$year_files[0]";
  206. my @month_files = dir_list("$dir/$year_files[0]", 'high_to_low',
  207. qr/^[0-9]{1,2}$/);
  208. return "$year_files[0]/$month_files[0]";
  209. }
  210. }
  211. # Below replaces:
  212. # my ($sec, $min, $hour, $mday, $mon,
  213. # $year, $wday, $yday, $isdst) = localtime(time);
  214. {
  215. my %name_map = (
  216. sec => 0, min => 1, hour => 2, mday => 3,
  217. mon => 4, year => 5, wday => 6, yday => 5,
  218. isdst => 6,
  219. );
  220. sub get_date {
  221. my (@names) = @_;
  222. my (@indices) = @name_map{@names};
  223. my (@values) = (localtime time)[@indices];
  224. return @values;
  225. }
  226. }
  227. =item month_before
  228. Return the month before the given month in the archive.
  229. Very naive; there has got to be a smarter way.
  230. =cut
  231. { my %cache; # cheap memoization
  232. sub month_before {
  233. my $self = shift;
  234. my ($this_month) = @_;
  235. if (exists $cache{$this_month}) {
  236. return $cache{$this_month};
  237. }
  238. my ($year, $month) = $this_month =~
  239. m/^ # start of string
  240. ([0-9]{4}) # 4 digit year
  241. \/ #
  242. ([0-9]{1,2}) # 2 digit month
  243. /x;
  244. if ($month == 1) {
  245. $month = 12;
  246. $year--;
  247. } else {
  248. $month--;
  249. }
  250. until (-e $self->local_path("$year/$month")) {
  251. if (! -d $self->local_path($year) ) {
  252. # Give up easily, wrapping to newest month.
  253. return $self->recent_month;
  254. }
  255. # handle January:
  256. if ($month == 1) {
  257. $month = 12;
  258. $year--;
  259. next;
  260. }
  261. $month--;
  262. }
  263. return $cache{$this_month} = "$year/$month";
  264. }
  265. }
  266. =item dir_list($dir, $sort_order, $pattern)
  267. Return a $sort_order sorted list of files matching regex $pattern in a
  268. directory.
  269. Calls $sort_order, which can be one of:
  270. alpha - alphabetical
  271. reverse_alpha - alphabetical, reversed
  272. high_to_low - numeric, high to low
  273. low_to_high - numeric, low to high
  274. =cut
  275. sub dir_list {
  276. my ($dir, $sort_order, $pattern) = @_;
  277. $pattern ||= qr/^[0-9]{1,2}$/;
  278. $sort_order ||= 'high_to_low';
  279. opendir my $list_dir, $dir
  280. or die "Couldn't open $dir: $!";
  281. my @files = sort $sort_order
  282. grep { m/$pattern/ }
  283. readdir $list_dir;
  284. closedir $list_dir;
  285. return @files;
  286. }
  287. # Various named sorts for dir_list:
  288. sub alpha { $a cmp $b; } # alphabetical
  289. sub high_to_low { $b <=> $a; } # numeric, high to low
  290. sub low_to_high { $a <=> $b; } # numberic, low to high
  291. sub reverse_alpha { $b cmp $a; } # alphabetical, reversed
  292. =item year($year)
  293. List out the updates for a year.
  294. =cut
  295. sub year {
  296. my $self = shift;
  297. my ($year) = @_;
  298. my ($year_file, $year_url) = $self->root_locations($year);
  299. # Year is a text file:
  300. return $self->entry_wrapped($year) if -T $year_file;
  301. # If it's not a directory, we can't do anything. Bail out:
  302. return p('No such year.') if (! -d $year_file);
  303. my $result;
  304. # Handle year directories with index files.
  305. $result .= $self->entry($year)
  306. if -T "$year_file/index";
  307. my $header_text = $self->icon_markup($year, $year);
  308. $header_text ||= q{};
  309. $result .= heading("$header_text $year", 3);
  310. my @months = dir_list($year_file, 'high_to_low', qr/^[0-9]{1,2}$/);
  311. my $year_text;
  312. my $count = 0; # explicitly defined for later printing.
  313. foreach my $month (@months) {
  314. my @entries = dir_list(
  315. "$year_file/$month", 'low_to_high', qr/^[0-9]{1,2}$/
  316. );
  317. $count += @entries;
  318. my $month_text;
  319. foreach my $entry (@entries) {
  320. $month_text .= a("href: $year_url/$month/$entry", $entry) . "\n";
  321. }
  322. $month_text = small("( $month_text )");
  323. my $link = a("href: $year_url/$month", month_name($month));
  324. $year_text .= table_row(
  325. table_cell('class: datelink', $link),
  326. table_cell('class: datelink', $month_text)
  327. ) . "\n\n";
  328. }
  329. $result .= "\n\n" . table($year_text) . "\n";
  330. if ($count > 1) {
  331. my $avg = int($count / @months);
  332. $result .= p("$count entries, roughly $avg an active month.");
  333. }
  334. elsif ($count == 0) { $result .= p("$count entries"); }
  335. elsif ($count == 1) { $result .= p("$count entry" ); }
  336. return entry_markup($result);
  337. }
  338. =item month($month)
  339. Prints the entries in a given month (nnnn/nn).
  340. =cut
  341. sub month {
  342. my $self = shift;
  343. my ($month) = @_;
  344. my ($month_file, $month_url) = $self->root_locations($month);
  345. my $result;
  346. # If a directory exists for $month, use dir_list to grab
  347. # the entry files it contains into @entry_files, sorted
  348. # numerically. Then send each entry to entry.
  349. if (-d $month_file) {
  350. $result .= $self->entry($month)
  351. if -T "$month_file/index";
  352. my @entry_files = dir_list ($month_file, 'high_to_low',
  353. qr/^[0-9]{1,2}$/);
  354. foreach my $entry_file (@entry_files) {
  355. $result .= entry_markup( $self->entry("$month/$entry_file")
  356. . $self->datestamp("$month/$entry_file") );
  357. }
  358. } elsif (-T $month_file) {
  359. $result .= $self->entry($month);
  360. }
  361. $result .= p( 'class: centerpiece',
  362. a('href: ' . $self->url_root . $self->month_before($month),
  363. 'previous')
  364. ) . "\n\n";
  365. return $result;
  366. }
  367. =item entry($entry)
  368. Returns the contents of a given entry. Calls dir_list
  369. and icon_markup. Recursively calls itself.
  370. =item entry_wrapped
  371. Wraps entry() in entry_markup.
  372. =item entry_stamped
  373. Wraps entry() + a datestamp in entry_markup()
  374. =cut
  375. sub entry_wrapped {
  376. my $self = shift;
  377. my ($entry, $level) = @_;
  378. return entry_markup($self->entry($entry, $level));
  379. }
  380. sub entry_stamped {
  381. my $self = shift;
  382. my ($entry, $level) = @_;
  383. return entry_markup(
  384. $self->entry($entry, $level)
  385. . $self->datestamp($entry)
  386. );
  387. }
  388. sub entry {
  389. my $self = shift;
  390. my ($entry, $level) = @_;
  391. $level ||= 'index';
  392. # Location of entry on local filesystem, and its URL:
  393. my ($entry_loc, $entry_url) = $self->root_locations($entry);
  394. my $result;
  395. # Display an icon, if we have one:
  396. if ( my $ico_markup = $self->icon_markup($entry) ) {
  397. $result .= heading($ico_markup, 2) . "\n\n";
  398. }
  399. # For text files:
  400. if (-T $entry_loc) {
  401. return $result . $self->fragment_slurp($entry_loc);
  402. }
  403. return $result if ! -d $entry_loc;
  404. # Print index as head:
  405. $result .= $self->fragment_slurp("$entry_loc/index");
  406. # Followed by any sub-entries:
  407. my @sub_entries = $self->get_sub_entries($entry_loc);
  408. if (@sub_entries >= 1) {
  409. if ($level eq 'index') {
  410. # Icons or text links:
  411. $result .= $self->list_contents($entry, @sub_entries);
  412. }
  413. elsif ($level eq 'all') {
  414. # Everything in the directory:
  415. foreach my $se (@sub_entries) {
  416. next if ($se =~ $self->binfile_expr);
  417. $result .= p('class: centerpiece', '+')
  418. . $self->entry("$entry/$se");
  419. }
  420. }
  421. }
  422. return $result;
  423. }
  424. sub get_sub_entries {
  425. my $self = shift;
  426. my ($entry_loc) = @_;
  427. my %ignore = ('index' => 1);
  428. return grep { ! $ignore{$_} }
  429. dir_list($entry_loc, 'alpha', $self->entryfile_expr);
  430. }
  431. sub list_contents {
  432. my $self = shift;
  433. my ($entry) = shift;
  434. my (@entries) = @_;
  435. my $contents;
  436. foreach my $se (@entries) {
  437. my $linktext = $self->icon_markup("$entry/$se", $se);
  438. $linktext ||= $se;
  439. $contents .= q{ }
  440. . a('href: ' . $self->url_root . "$entry/$se",
  441. $linktext,
  442. "title: $se");
  443. }
  444. return p( em('more') . ": $contents" ) . "\n";
  445. }
  446. =item icon_markup
  447. Check if an icon exists for a given entry if so, return markup to include it.
  448. Icons are PNG or JPEG image files following a specific naming convention:
  449. index.icon.[png|jp(e)g] for directories
  450. [filename].icon.[png|jp(e)g] for flat text files
  451. Calls image_size, uses filename to determine type.
  452. =cut
  453. { my %cache;
  454. sub icon_markup {
  455. my $self = shift;
  456. my ($entry, $alt) = @_;
  457. if ($cache{$entry . $alt}) {
  458. return $cache{$entry.$alt};
  459. }
  460. my ($entry_loc, $entry_url) = $self->root_locations($entry);
  461. my ($icon_loc, $icon_url);
  462. if (-T $entry_loc) {
  463. $icon_loc = "$entry_loc.icon";
  464. $icon_url = "$entry_url.icon";
  465. }
  466. elsif (-d $entry_loc) {
  467. $icon_loc = "$entry_loc/index.icon";
  468. $icon_url = "$entry_url/index.icon";
  469. }
  470. # First suffix found will be used:
  471. my (@suffixes) = qw(png gif jpg jpeg);
  472. my $suffix;
  473. for (@suffixes) {
  474. if (-e "$icon_loc.$_") {
  475. $suffix = $_;
  476. last;
  477. }
  478. }
  479. # fail unless there's a file with one of the above suffixes
  480. return 0 unless $suffix;
  481. # call image_size to slurp width & height from the image file
  482. my ($width, $height) = image_size("$icon_loc.$suffix");
  483. return $cache{$entry . $alt} =
  484. qq{<img src="$icon_url.$suffix"\n width="$width" }
  485. . qq{height="$height"\n alt="$alt" />};
  486. }
  487. }
  488. =item datestamp
  489. Returns a nice html datestamp for a given entry, including a wikilink for
  490. discussion and suchlike.
  491. =cut
  492. sub datestamp {
  493. my $self = shift;
  494. my ($entry) = @_;
  495. my ($stamp);
  496. if ( $entry =~ m{(^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2})}x ) {
  497. my ($entry_year, $entry_month, $entry_day) = split m{/}, $1;
  498. # this stuff conditionalizes the wikilink
  499. # so that if nothing exists, you wind up with an edit form
  500. my ($wiki_date_name) = month_name($entry_month)
  501. . "_${entry_day}_${entry_year}";
  502. my $wikistamp = ':: ';
  503. my $wikititle;
  504. if ($self->wala->is_page($wiki_date_name)) {
  505. $wikititle = 'read the margins';
  506. } else { $wikititle = 'write in the margins'; }
  507. $wikistamp .= a("href: " . $self->wala->ScriptName . "?$wiki_date_name",
  508. $wikititle,
  509. 'title: a page you can edit');
  510. # Return a fancy datestamp:
  511. my $month_name = month_name($entry_month);
  512. my $year_url = "href: " . $self->url_root . $entry_year;
  513. $stamp = "\n "
  514. . a($year_url, $entry_year,
  515. "title: $entry_year") . "\n "
  516. . a("$year_url/$entry_month", $month_name,
  517. "title: $entry_year/$entry_month") . "\n "
  518. . a("$year_url/$entry_month/$entry_day", $entry_day,
  519. "title: $entry_year/$entry_month/$entry_day") . "\n "
  520. . $wikistamp . "\n";
  521. } else {
  522. $stamp = "(failed to construct datestamp for $entry)";
  523. }
  524. return p('class: datelink', $stamp);
  525. }
  526. =item fragment_slurp
  527. Read a text fragment, call line_parse to take care of funky markup and
  528. interpreting embedded code, and then return it as a string. Takes one
  529. parameter, the name of the file, and returns '' if it's not an extant text
  530. file.
  531. This might be the place to implement an in-memory cache for FastCGI or mod_perl
  532. environments. The trick is that the line_parse() results for certain files
  533. shouldn't be cached because they contain embedded code.
  534. =cut
  535. sub fragment_slurp {
  536. my $self = shift;
  537. my ($file) = @_;
  538. return q{} if (! -T $file);
  539. # $file is text:
  540. my $everything;
  541. open my $fh, '<', $file
  542. or die "Couldn't open $file: $!\n";
  543. {
  544. # line sep
  545. local $/ = undef;
  546. $everything = <$fh>;
  547. }
  548. close $fh or die "Couldn't close: $!";
  549. # eval embedded Perl and ${variables}:
  550. $self->eval_perl($everything);
  551. # Take care of any special markup.
  552. # We pass along $file so it has some context to work with
  553. return $self->line_parse($everything, $file);
  554. }
  555. =item eval_perl
  556. Evaluate embedded Perl in a string, replacing blocks enclosed with <perl> tags
  557. with whatever they return (well, evaluated in a scalar context). Modifies
  558. a string in-place, so be careful.
  559. Also handles simple ${variables}, replacing them from the keys to $self.
  560. =cut
  561. sub eval_perl {
  562. my $self = shift;
  563. while ($_[0] =~ m{<perl>(.*?)</perl>}s) {
  564. my $block = $1;
  565. # Run the $block, and include anything returned -
  566. # or an error message, if we got one.
  567. my $output = eval $block;
  568. $output = $@ if $@;
  569. $_[0] =~ s{<perl>\Q$block\E</perl>}{$output}s;
  570. }
  571. # Interpolate variables:
  572. $_[0] =~ s/\${([a-zA-Z_]+)}/$self->{$1}/ge;
  573. return;
  574. }
  575. =item month_name
  576. Turn numeric dates into English.
  577. =cut
  578. sub month_name {
  579. my ($number) = @_;
  580. # "Null" is here so that $month_name[1] corresponds to January, etc.
  581. my @months = qw(Null January February March April May June
  582. July August September October November December);
  583. return $months[$number];
  584. }
  585. =item root_locations($file)
  586. =item
  587. Given a file/entry, return the appropriate concatenations with
  588. root_dir and url_root.
  589. =cut
  590. sub root_locations {
  591. return (
  592. $_[0]->local_path($_[1]),
  593. $_[0]->url_root . $_[1]
  594. );
  595. }
  596. =item local_path
  597. Return an absolute path for a given file. Called by root_locations.
  598. Arguably this is stupid and inefficient.
  599. =cut
  600. sub local_path {
  601. return $_[0]->root_dir . '/' . $_[1];
  602. }
  603. =item feed_print
  604. Return an Atom feed of entries for a month. Defaults to the most
  605. recent month in the archive.
  606. Called from handle(), requires XML::Atom::SimpleFeed.
  607. =cut
  608. sub feed_print {
  609. my $self = shift;
  610. my ($month) = @_;
  611. $month ||= $self->recent_month();
  612. my $feed_url = $self->url_root . $self->feed_alias;
  613. my ($month_file, $month_url) = $self->root_locations($month);
  614. my $feed = XML::Atom::SimpleFeed->new(
  615. title => $self->title,
  616. link => $self->url_root,
  617. link => { rel => 'self', href => $feed_url, },
  618. icon => $self->favicon_url,
  619. author => $self->author,
  620. id => $self->url_root,
  621. generator => 'Display.pm / XML::Atom::SimpleFeed',
  622. );
  623. my @entry_files;
  624. if (-d $month_file) {
  625. @entry_files = dir_list ($month_file,
  626. 'high_to_low',
  627. qr/^[0-9]{1,2}$/);
  628. } else {
  629. return 0;
  630. }
  631. foreach my $entry_file (@entry_files) {
  632. my $entry = "$month/$entry_file";
  633. my $entry_url = $month_url . "/$entry_file";
  634. $feed->add_entry(
  635. title => $entry,
  636. link => $entry_url,
  637. id => $entry_url,
  638. content => $self->entry($entry),
  639. );
  640. }
  641. return "Content-type: application/atom+xml\n\n"
  642. . $feed->as_string;
  643. }
  644. =back
  645. =head1 SEE ALSO
  646. walawiki.org, Blosxom, rassmalog, Text::Textile, XML::Atom::SimpleFeed,
  647. Image::Size, CGI::Fast.
  648. =head1 AUTHOR
  649. Copyright 2001-2007 Brennen Bearnes
  650. Image sizing code (in image_size) derived from wwwis, by Alex Knowles and
  651. Andrew Tong.
  652. display.pl is free software; you can redistribute it and/or modify
  653. it under the terms of the GNU General Public License as published by
  654. the Free Software Foundation; either version 2 of the License, or
  655. (at your option) any later version.
  656. This program is distributed in the hope that it will be useful,
  657. but WITHOUT ANY WARRANTY; without even the implied warranty of
  658. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  659. GNU General Public License for more details.
  660. =cut
  661. 1;