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.

1183 lines
29 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
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. =pod
  7. =head1 NAME
  8. Display - module to display fragments of text on the web and elsewhere
  9. =head1 SYNOPSIS
  10. #!/usr/bin/perl
  11. use Display;
  12. my $d = Display->new(
  13. root_dir => 'archives',
  14. url_root => '/display.pl?',
  15. # etc.
  16. );
  17. print $d->handle(@ARGV);
  18. =head1 DESCRIPTION
  19. Display started life as a simple script to concatenate fragments of handwritten
  20. HTML by date. It has since haphazardly accumulated several of the usual weblog
  21. features (comments, lightweight markup, feed generation, embedded Perl, poetry
  22. tools, image galleries, and ill-advised dependencies), but the basic idea
  23. hasn't changed much.
  24. The module will work with FastCGI, if called from the appropriate wrapper
  25. script. If you use CGI::Fast, you can pass query objects directly to
  26. C<handle()>.
  27. By default, entries are stored in a simple directory tree under C<root_dir>.
  28. Like:
  29. archives/2001/1/1
  30. archives/2001/1/1/sub_entry
  31. It is possible (although not yet as flexible as it ought to be) to redefine
  32. the directory layout. More about this after a bit.
  33. An entry may be either a plain text file, or a directory containing several
  34. files. If it's a directory, a file named "index" will be treated as the text
  35. of the entry, and all other lower-case filenames without extensions will be
  36. treated as sub-entries or documents within that entry, and displayed
  37. accordingly. Links to certain other filetypes will be displayed as well.
  38. Directories may be nested to an arbitrary depth, although it's probably not a
  39. good idea to go very deep with the current display logic.
  40. A PNG or JPEG file with a name like
  41. 2001/1/1.icon.png
  42. 2001/1/1/index.icon.png
  43. 2001/1/1/whatever.icon.png
  44. 2001/1/1/whatever/index.icon.png
  45. will be treated as an icon for the appropriate entry file.
  46. =head2 MARKUP
  47. Entries may consist of hand-written HTML (to be passed along without further
  48. interpretation), a supported form of lightweight markup, or some combination
  49. thereof. Actually, an entry may consist of any darn thing you please, as long
  50. as Perl will agree that it is text, but presumably you're going to be feeding
  51. this to a browser.
  52. Special markup is indicated by a variety of HTML-like container tags.
  53. B<Embedded Perl> - evaluated and replaced by whatever value you return
  54. (evaluated in a scalar context):
  55. <perl>my $dog = "Ralph."; return $dog;</perl>
  56. This code is evaluated before any other processing is done, so you can return
  57. any other markup understood by the script and have it handled appropriately.
  58. B<Interpolated variables> - actually keys to the hash underlying the Display
  59. object, for the moment:
  60. <perl>$self->title("About Ralph, My Dog"); return '';</perl>
  61. <p>The title is <em>${title}</em>.</p>
  62. This will change.
  63. Embedded code and variables are intended for use in F<header> and F<footer>
  64. files, where it's handy to drop in titles or conditionalize aspects of a
  65. layout. You want to be careful with this sort of thing - it's useful in small
  66. doses, but it's also a maintainability nightmare waiting to happen.
  67. (WordPress, I am looking at you.)
  68. B<Several forms of lightweight markup>:
  69. <wala>Wala::Markup, via Wala.pm - very basic wiki syntax</wala>
  70. <textile>Dean Allen's Textile, via Brad Choate's
  71. Text::Textile.</textile>
  72. <freeverse>An easy way to
  73. get properly broken lines
  74. plus -- en and em dashes ---
  75. for poetry and such.</freeverse>
  76. B<And a couple of shortcuts>:
  77. <image>filename.ext
  78. alt text, if any</image>
  79. <list>
  80. one list item
  81. another list item
  82. </list>
  83. As it stands, freeverse, image, and list are not particularly robust.
  84. =head1 NAME
  85. Display - module to display fragments of text on the web and elsewhere
  86. =head1 SYNOPSIS
  87. #!/usr/bin/perl
  88. use Display;
  89. my $d = Display->new(
  90. root_dir => 'archives',
  91. url_root => '/display.pl?',
  92. # etc.
  93. );
  94. print $d->handle(@ARGV);
  95. =head1 DESCRIPTION
  96. Display started life as a simple script to concatenate fragments of handwritten
  97. HTML by date. It has since haphazardly accumulated several of the usual weblog
  98. features (comments, lightweight markup, feed generation, embedded Perl, poetry
  99. tools, image galleries, and ill-advised dependencies), but the basic idea
  100. hasn't changed much.
  101. The module will work with FastCGI, if called from the appropriate wrapper
  102. script. If you use CGI::Fast, you can pass query objects directly to
  103. C<handle()>.
  104. By default, entries are stored in a simple directory tree under C<root_dir>.
  105. Like:
  106. archives/2001/1/1
  107. archives/2001/1/1/sub_entry
  108. It is possible (although not yet as flexible as it ought to be) to redefine
  109. the directory layout. More about this after a bit.
  110. An entry may be either a plain text file, or a directory containing several
  111. files. If it's a directory, a file named "index" will be treated as the text
  112. of the entry, and all other lower-case filenames without extensions will be
  113. treated as sub-entries or documents within that entry, and displayed
  114. accordingly. Links to certain other filetypes will be displayed as well.
  115. Directories may be nested to an arbitrary depth, although it's probably not a
  116. good idea to go very deep with the current display logic.
  117. A PNG or JPEG file with a name like
  118. 2001/1/1.icon.png
  119. 2001/1/1/index.icon.png
  120. 2001/1/1/whatever.icon.png
  121. 2001/1/1/whatever/index.icon.png
  122. will be treated as an icon for the appropriate entry file.
  123. =head2 MARKUP
  124. Entries may consist of hand-written HTML (to be passed along without further
  125. interpretation), a supported form of lightweight markup, or some combination
  126. thereof. Actually, an entry may consist of any darn thing you please, as long
  127. as Perl will agree that it is text, but presumably you're going to be feeding
  128. this to a browser.
  129. Special markup is indicated by a variety of HTML-like container tags.
  130. B<Embedded Perl> - evaluated and replaced by whatever value you return
  131. (evaluated in a scalar context):
  132. <perl>my $dog = "Ralph."; return $dog;</perl>
  133. This code is evaluated before any other processing is done, so you can return
  134. any other markup understood by the script and have it handled appropriately.
  135. B<Interpolated variables> - actually keys to the hash underlying the Display
  136. object, for the moment:
  137. <perl>$self->title("About Ralph, My Dog"); return '';</perl>
  138. <p>The title is <em>${title}</em>.</p>
  139. This will change.
  140. Embedded code and variables are intended for use in F<header> and F<footer>
  141. files, where it's handy to drop in titles or conditionalize aspects of a
  142. layout. You want to be careful with this sort of thing - it's useful in small
  143. doses, but it's also a maintainability nightmare waiting to happen.
  144. (WordPress, I am looking at you.)
  145. B<Several forms of lightweight markup>:
  146. <wala>Wala::Markup, via Wala.pm - very basic wiki syntax</wala>
  147. <textile>Dean Allen's Textile, via Brad Choate's
  148. Text::Textile.</textile>
  149. <freeverse>An easy way to
  150. get properly broken lines
  151. plus -- en and em dashes ---
  152. for poetry and such.</freeverse>
  153. B<And a couple of shortcuts>:
  154. <image>filename.ext
  155. alt text, if any</image>
  156. <list>
  157. one list item
  158. another list item
  159. </list>
  160. As it stands, freeverse, image, and list are not particularly robust.
  161. =cut
  162. use strict;
  163. use warnings;
  164. no warnings 'uninitialized';
  165. use base 'MethodSpit';
  166. use XML::Atom::SimpleFeed;
  167. use Wala;
  168. use Display::HTML qw(:highlevel);
  169. use Display::Markup qw(line_parse image_markup);
  170. use Display::Image qw(image_size);
  171. =head1 CONFIGURATION
  172. =over
  173. =item options
  174. See F<conf.pl> for a sample configuration.
  175. =cut
  176. my %default = (
  177. root_dir => 'archives', # root dir for archived files
  178. url_root => "$0?", # root URL for building links
  179. image_url_root => '', # same for images
  180. header => 'header',
  181. footer => 'footer',
  182. title => '',
  183. stylesheet_url => undef,
  184. favicon_url => undef,
  185. feed_alias => 'feed',
  186. author => undef,
  187. description => undef,
  188. license => undef,
  189. http_header => 1,
  190. default_entry => 'new',
  191. # What gets considered an entry file:
  192. entryfile_expr => qr/^[a-z_]+(\.(tgz|zip|tar[.]gz|gz|txt))?$/,
  193. # We'll show links for these, but not display them inline:
  194. binfile_expr => qr/[.](tgz|zip|tar[.]gz|gz|txt|pdf)$/,
  195. wala => Wala->new(),
  196. );
  197. =item entry_map(\%map)
  198. Takes a hashref which will dispatch entries matching various regexen to
  199. the appropriate output methods. The default looks something like this:
  200. nnnn/[nn/nn/]doc_name - a document within a day.
  201. nnnn/nn/nn - a specific day.
  202. nnnn/nn - a month.
  203. nnnn - a year.
  204. doc_name - a document in the root directory.
  205. You can re-map things to an arbitrary archive layout.
  206. Since the entry map is a hash, and handle() simply loops over its keys, there
  207. is no guaranteed precedence of patterns. Be extremely careful that no entry
  208. will match more than one pattern, or you will wind up with unexpected behavior.
  209. A good way to ensure that this does not happen is to use patterns like:
  210. qr(
  211. ^ # start of string
  212. [0-9/]{4}/ # year
  213. [0-9]{1,2}/ # month
  214. [0-9]{1,2] # day
  215. $ # end of string
  216. )x
  217. ...always marking the start and end of the string explicitly.
  218. =cut
  219. $default{entry_map} = {
  220. qr'^[0-9/]{5,11}[a-z_/]+$' => sub { entry_stamped (@_ ) },
  221. qr'^[0-9]{4}/[0-9]{1,2}/
  222. [0-9]{1,2}$'x => sub { entry_stamped (@_, 'all') },
  223. qr'^[0-9]{4}/[0-9]{1,2}$' => sub { month (@_ ) },
  224. qr'^[0-9]{4}$' => sub { year (@_ ) },
  225. qr'^[a-z_]' => sub { entry_wrapped (@_, 'all') },
  226. };
  227. # Set up some accessor methods:
  228. __PACKAGE__->methodspit( keys %default );
  229. =back
  230. =head1 METHODS
  231. For no bigger than this thing is, it gets a little convoluted.
  232. =over
  233. =item new(%params)
  234. Get a new Display object with the specified parameters set.
  235. =cut
  236. sub new {
  237. my $class = shift;
  238. my %params = @_;
  239. my $self = \%default;
  240. bless $self, $class;
  241. $self->configure(%params);
  242. return $self;
  243. }
  244. =item configure(param => 'value')
  245. Set specified parameters.
  246. =cut
  247. sub configure {
  248. my $self = shift;
  249. my %params = @_;
  250. for my $p (keys %params) {
  251. $self->{$p} = $params{$p};
  252. }
  253. return;
  254. }
  255. =item walaconf(%options)
  256. Set parameters for Wala.pm.
  257. =cut
  258. sub walaconf {
  259. my $self = shift;
  260. $self->wala->conf(@_);
  261. return;
  262. }
  263. =item display($entry1, $entry2, ...)
  264. Return a string containing the given entries, which can be in the form of CGI
  265. query objects or date/entry strings. If no parameters are given, default to
  266. default_entry().
  267. display() expands aliases ("new" and "all") and CGI query objects as necessary,
  268. collects input from handle($entry), and wraps the whole thing in header and
  269. footer files.
  270. =cut
  271. sub display {
  272. my $self = shift;
  273. my (@options) = @_;
  274. # Get parameters from any CGI queries, make sure we have at least the
  275. # default, and expand on any aliases:
  276. @options = map { expand_query($_) } @options;
  277. $options[0] ||= $self->default_entry;
  278. $self->title(join ' ', @options); # title for head/foot
  279. @options = map { $self->expand_option($_) } @options;
  280. my $output;
  281. for my $option (@options) {
  282. return $self->feed_print() if $option eq $self->feed_alias;
  283. $output .= $self->handle($option);
  284. }
  285. # Wrap entries in header/footer:
  286. my $header;
  287. $header .= "Content-Type: text/html\n\n" if $self->http_header;
  288. $header .= $self->fragment_slurp($self->header);
  289. return $header
  290. . $output
  291. . $self->fragment_slurp($self->footer);
  292. }
  293. =item handle($entry)
  294. Return the text of an individual entry.
  295. =begin digression
  296. =item A digression about each()
  297. I just spent a lot of time chasing down a bug caused by the while loop
  298. below. Specifically, since $self->entry_map returns a reference to the
  299. same hash each time, every other request was finding each() mid-way
  300. through iterating over this hash.
  301. I solved this by copying this hash into a local one called %map every
  302. time handle() is called. Another approach would be to call keys() or
  303. values on the anonymous hash referenced by $self->entry_map, which
  304. apparently resets each().
  305. =end digression
  306. =cut
  307. sub handle {
  308. my $self = shift;
  309. my ($option) = @_;
  310. # Hashref:
  311. my $map = $self->entry_map;
  312. # Take the first matching pattern:
  313. my ($pattern) = grep { $option =~ $_ } keys %{ $map };
  314. return unless defined $pattern;
  315. return $map->{$pattern}->($self, $option);
  316. }
  317. =item expand_query
  318. Expands a CGI query object (for example, one passed in from CGI::Fast) to an
  319. appropriate list of parameters.
  320. =cut
  321. sub expand_query {
  322. my ($option) = shift;
  323. if ( (ref $option eq 'CGI::Fast') or (ref $option eq 'CGI') ) {
  324. return $option->param('keywords');
  325. } else {
  326. return $option;
  327. }
  328. }
  329. =item expand_option
  330. Expands/converts 'all' and 'new' to appropriate values.
  331. =cut
  332. sub expand_option {
  333. my ($self, $option) = @_;
  334. # Take care of trailing slashes:
  335. #chop $option if substr($option, -1, 1) eq q{/};
  336. chop $option if $option =~ m{/$};
  337. if ($option eq 'all') {
  338. return dir_list($self->root_dir, 'high_to_low', qr/^[0-9]{1,4}$/);
  339. } elsif ($option eq 'new') {
  340. return $self->recent_month;
  341. } else {
  342. return $option;
  343. }
  344. }
  345. =item recent_month
  346. Tries to find the most recent month in the archive.
  347. If a year file is text, returns that instead.
  348. =cut
  349. sub recent_month {
  350. my $self = shift;
  351. my ($dir) = $self->root_dir;
  352. my ($mon, $year) = get_date('mon', 'year');
  353. $mon++;
  354. $year += 1900;
  355. if (-e "$dir/$year/$mon") {
  356. return "$year/$mon";
  357. }
  358. else {
  359. my @year_files = dir_list($dir, 'high_to_low', qr/^[0-9]{1,4}$/);
  360. return $year_files[0] if -T "$dir/$year_files[0]";
  361. my @month_files = dir_list("$dir/$year_files[0]", 'high_to_low',
  362. qr/^[0-9]{1,2}$/);
  363. return "$year_files[0]/$month_files[0]";
  364. }
  365. }
  366. # Below replaces:
  367. # my ($sec, $min, $hour, $mday, $mon,
  368. # $year, $wday, $yday, $isdst) = localtime(time);
  369. {
  370. my %name_map = (
  371. sec => 0, min => 1, hour => 2, mday => 3,
  372. mon => 4, year => 5, wday => 6, yday => 5,
  373. isdst => 6,
  374. );
  375. sub get_date {
  376. my (@names) = @_;
  377. my (@indices) = @name_map{@names};
  378. my (@values) = (localtime time)[@indices];
  379. return @values;
  380. }
  381. }
  382. =item month_before
  383. Return the month before the given month in the archive.
  384. Very naive; there has got to be a smarter way.
  385. =cut
  386. { my %cache; # cheap memoization
  387. sub month_before {
  388. my $self = shift;
  389. my ($this_month) = @_;
  390. if (exists $cache{$this_month}) {
  391. return $cache{$this_month};
  392. }
  393. my ($year, $month) = $this_month =~
  394. m/^ # start of string
  395. ([0-9]{4}) # 4 digit year
  396. \/ #
  397. ([0-9]{1,2}) # 2 digit month
  398. /x;
  399. if ($month == 1) {
  400. $month = 12; $year--;
  401. } else {
  402. $month--;
  403. }
  404. until (-e $self->local_path("$year/$month")) {
  405. if (! -d $self->local_path($year) ) {
  406. # Give up easily, wrapping to newest month.
  407. return $self->recent_month;
  408. }
  409. # handle January:
  410. if ($month == 1) {
  411. $month = 12; $year--;
  412. next;
  413. }
  414. $month--;
  415. }
  416. return $cache{$this_month} = "$year/$month";
  417. }
  418. }
  419. =item dir_list($dir, $sort_order, $pattern)
  420. Return a $sort_order sorted list of files matching regex $pattern in a
  421. directory.
  422. Calls $sort_order, which can be one of:
  423. alpha - alphabetical
  424. reverse_alpha - alphabetical, reversed
  425. high_to_low - numeric, high to low
  426. low_to_high - numeric, low to high
  427. =cut
  428. sub dir_list {
  429. my ($dir, $sort_order, $pattern) = @_;
  430. $pattern ||= qr/^[0-9]{1,2}$/;
  431. $sort_order ||= 'high_to_low';
  432. opendir my $list_dir, $dir
  433. or die "Couldn't open $dir: $!";
  434. my @files = sort $sort_order
  435. grep { m/$pattern/ }
  436. readdir $list_dir;
  437. closedir $list_dir;
  438. return @files;
  439. }
  440. # Various named sorts for dir_list:
  441. sub alpha { $a cmp $b; } # alphabetical
  442. sub high_to_low { $b <=> $a; } # numeric, high to low
  443. sub low_to_high { $a <=> $b; } # numberic, low to high
  444. sub reverse_alpha { $b cmp $a; } # alphabetical, reversed
  445. =item year($year)
  446. List out the updates for a year.
  447. =cut
  448. sub year {
  449. my $self = shift;
  450. my ($year) = @_;
  451. my ($year_file, $year_url) = $self->root_locations($year);
  452. # Year is a text file:
  453. return $self->entry_wrapped($year) if -T $year_file;
  454. # If it's not a directory, we can't do anything. Bail out:
  455. return p('No such year.') if (! -d $year_file);
  456. my $result;
  457. # Handle year directories with index files.
  458. $result .= $self->entry($year)
  459. if -T "$year_file/index";
  460. my $header_text = $self->icon_markup($year, $year);
  461. $header_text ||= q{};
  462. $result .= heading("$header_text $year", 3);
  463. my @months = dir_list($year_file, 'high_to_low', qr/^[0-9]{1,2}$/);
  464. my $year_text;
  465. my $count = 0; # explicitly defined for later printing.
  466. foreach my $month (@months) {
  467. my @entries = dir_list(
  468. "$year_file/$month", 'low_to_high', qr/^[0-9]{1,2}$/
  469. );
  470. $count += @entries;
  471. my $month_text;
  472. foreach my $entry (@entries) {
  473. $month_text .= a("href: $year_url/$month/$entry", $entry) . "\n";
  474. }
  475. $month_text = small("( $month_text )");
  476. my $link = a("href: $year_url/$month", month_name($month));
  477. $year_text .= table_row(
  478. table_cell('class: datelink', $link),
  479. table_cell('class: datelink', $month_text)
  480. ) . "\n\n";
  481. }
  482. $result .= "\n\n" . table($year_text) . "\n";
  483. if ($count > 1) {
  484. my $avg = int($count / @months);
  485. $result .= p("$count entries, roughly $avg an active month.");
  486. }
  487. elsif ($count == 0) { $result .= p("$count entries"); }
  488. elsif ($count == 1) { $result .= p("$count entry" ); }
  489. return entry_markup($result);
  490. }
  491. =item month($month)
  492. Prints the entries in a given month (nnnn/nn).
  493. =cut
  494. sub month {
  495. my $self = shift;
  496. my ($month) = @_;
  497. my ($month_file, $month_url) = $self->root_locations($month);
  498. my $result;
  499. # If a directory exists for $month, use dir_list to grab
  500. # the entry files it contains into @entry_files, sorted
  501. # numerically. Then send each entry to entry.
  502. if (-d $month_file) {
  503. $result .= $self->entry($month)
  504. if -T "$month_file/index";
  505. my @entry_files = dir_list ($month_file, 'high_to_low',
  506. qr/^[0-9]{1,2}$/);
  507. foreach my $entry_file (@entry_files) {
  508. $result .= entry_markup( $self->entry("$month/$entry_file")
  509. . $self->datestamp("$month/$entry_file") );
  510. }
  511. } elsif (-T $month_file) {
  512. $result .= $self->entry($month);
  513. }
  514. $result .= entry_markup(
  515. p( 'class: navigation',
  516. a('href: ' . $self->url_root . $self->month_before($month),
  517. 'title: previous month',
  518. '&#8656;')
  519. ) . "\n\n"
  520. );
  521. return $result;
  522. }
  523. =item entry($entry)
  524. Returns the contents of a given entry. Calls dir_list
  525. and icon_markup. Recursively calls itself.
  526. =item entry_wrapped
  527. Wraps entry() in entry_markup.
  528. =item entry_stamped
  529. Wraps entry() + a datestamp in entry_markup()
  530. =cut
  531. sub entry_wrapped {
  532. my $self = shift;
  533. my ($entry, $level) = @_;
  534. return entry_markup($self->entry($entry, $level));
  535. }
  536. sub entry_stamped {
  537. my $self = shift;
  538. my ($entry, $level) = @_;
  539. return entry_markup(
  540. $self->entry($entry, $level)
  541. . $self->datestamp($entry)
  542. );
  543. }
  544. sub entry {
  545. my $self = shift;
  546. my ($entry, $level) = @_;
  547. $level ||= 'index';
  548. # Location of entry on local filesystem, and its URL:
  549. my ($entry_loc, $entry_url) = $self->root_locations($entry);
  550. my $result;
  551. # Display an icon, if we have one:
  552. if ( my $ico_markup = $self->icon_markup($entry) ) {
  553. $result .= heading($ico_markup, 2) . "\n\n";
  554. }
  555. # For text files:
  556. if (-T $entry_loc) {
  557. return $result . $self->fragment_slurp($entry_loc);
  558. }
  559. return $result if ! -d $entry_loc;
  560. # Print index as head:
  561. $result .= $self->fragment_slurp("$entry_loc/index");
  562. # Followed by any sub-entries:
  563. my @sub_entries = $self->get_sub_entries($entry_loc);
  564. if (@sub_entries >= 1) {
  565. if ($level eq 'index') {
  566. # Icons or text links:
  567. $result .= $self->list_contents($entry, @sub_entries);
  568. }
  569. elsif ($level eq 'all') {
  570. # Everything in the directory:
  571. foreach my $se (@sub_entries) {
  572. next if ($se =~ $self->binfile_expr);
  573. $result .= p('class: centerpiece', '+')
  574. . $self->entry("$entry/$se");
  575. }
  576. }
  577. }
  578. return $result;
  579. }
  580. sub get_sub_entries {
  581. my $self = shift;
  582. my ($entry_loc) = @_;
  583. my %ignore = ('index' => 1);
  584. return grep { ! $ignore{$_} }
  585. dir_list($entry_loc, 'alpha', $self->entryfile_expr);
  586. }
  587. sub list_contents {
  588. my $self = shift;
  589. my ($entry) = shift;
  590. my (@entries) = @_;
  591. my $contents;
  592. foreach my $se (@entries) {
  593. my $linktext = $self->icon_markup("$entry/$se", $se);
  594. $linktext ||= $se;
  595. $contents .= q{ }
  596. . a('href: ' . $self->url_root . "$entry/$se",
  597. $linktext,
  598. "title: $se");
  599. }
  600. return p( em('more') . ": $contents" ) . "\n";
  601. }
  602. =item icon_markup
  603. Check if an icon exists for a given entry if so, return markup to include it.
  604. Icons are PNG or JPEG image files following a specific naming convention:
  605. index.icon.[png|jp(e)g] for directories
  606. [filename].icon.[png|jp(e)g] for flat text files
  607. Calls image_size, uses filename to determine type.
  608. =cut
  609. { my %cache;
  610. sub icon_markup {
  611. my $self = shift;
  612. my ($entry, $alt) = @_;
  613. if ($cache{$entry . $alt}) {
  614. return $cache{$entry.$alt};
  615. }
  616. my ($entry_loc, $entry_url) = $self->root_locations($entry);
  617. my ($icon_loc, $icon_url);
  618. if (-T $entry_loc) {
  619. $icon_loc = "$entry_loc.icon";
  620. $icon_url = "$entry_url.icon";
  621. }
  622. elsif (-d $entry_loc) {
  623. $icon_loc = "$entry_loc/index.icon";
  624. $icon_url = "$entry_url/index.icon";
  625. }
  626. # First suffix found will be used:
  627. my (@suffixes) = qw(png gif jpg jpeg);
  628. my $suffix;
  629. for (@suffixes) {
  630. if (-e "$icon_loc.$_") {
  631. $suffix = $_;
  632. last;
  633. }
  634. }
  635. # fail unless there's a file with one of the above suffixes
  636. return 0 unless $suffix;
  637. # call image_size to slurp width & height from the image file
  638. my ($width, $height) = image_size("$icon_loc.$suffix");
  639. return $cache{$entry . $alt} =
  640. qq{<img src="$icon_url.$suffix"\n width="$width" }
  641. . qq{height="$height"\n alt="$alt" />};
  642. }
  643. }
  644. =item datestamp
  645. Returns a nice html datestamp for a given entry, including a wikilink for
  646. discussion and suchlike.
  647. =cut
  648. sub datestamp {
  649. my $self = shift;
  650. my ($entry) = @_;
  651. my ($stamp);
  652. if ( $entry =~ m{(^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2})}x ) {
  653. my ($entry_year, $entry_month, $entry_day) = split m{/}, $1;
  654. # this stuff conditionalizes the wikilink
  655. # so that if nothing exists, you wind up with an edit form
  656. my ($wiki_date_name) = month_name($entry_month)
  657. . "_${entry_day}_${entry_year}";
  658. my $wikistamp = ':: ';
  659. my $wikititle;
  660. if ($self->wala->is_page($wiki_date_name)) {
  661. $wikititle = 'read the margins';
  662. } else { $wikititle = 'write in the margins'; }
  663. $wikistamp .= a("href: " . $self->wala->ScriptName . "?$wiki_date_name",
  664. $wikititle,
  665. 'title: a page you can edit');
  666. if ( -e $self->local_path($entry . "/NoMargin") ) {
  667. $wikistamp = "<!-- Margin blocked. -->";
  668. }
  669. # Return a fancy datestamp:
  670. my $month_name = month_name($entry_month);
  671. my $year_url = "href: " . $self->url_root . $entry_year;
  672. $stamp = "\n "
  673. . a($year_url, $entry_year,
  674. "title: $entry_year") . "\n "
  675. . a("$year_url/$entry_month", $month_name,
  676. "title: $entry_year/$entry_month") . "\n "
  677. . a("$year_url/$entry_month/$entry_day", $entry_day,
  678. "title: $entry_year/$entry_month/$entry_day") . "\n "
  679. . $wikistamp . "\n";
  680. } else {
  681. $stamp = "(failed to construct datestamp for $entry)";
  682. }
  683. return p('class: datelink', $stamp);
  684. }
  685. =item fragment_slurp
  686. Read a text fragment, call line_parse to take care of funky markup and
  687. interpreting embedded code, and then return it as a string. Takes one
  688. parameter, the name of the file, and returns '' if it's not an extant text
  689. file.
  690. This might be the place to implement an in-memory cache for FastCGI or mod_perl
  691. environments. The trick is that the line_parse() results for certain files
  692. shouldn't be cached because they contain embedded code.
  693. =cut
  694. sub fragment_slurp {
  695. my $self = shift;
  696. my ($file) = @_;
  697. return q{} if (! -T $file);
  698. # $file is text:
  699. my $everything;
  700. open my $fh, '<', $file
  701. or die "Couldn't open $file: $!\n";
  702. {
  703. # line sep
  704. local $/ = undef;
  705. $everything = <$fh>;
  706. }
  707. close $fh or die "Couldn't close: $!";
  708. # eval embedded Perl and ${variables}:
  709. $self->eval_perl($everything);
  710. # Take care of any special markup.
  711. # We pass along $file so it has some context to work with
  712. return $self->line_parse($everything, $file);
  713. }
  714. =item eval_perl
  715. Evaluate embedded Perl in a string, replacing blocks enclosed with <perl> tags
  716. with whatever they return (well, evaluated in a scalar context). Modifies
  717. a string in-place, so be careful.
  718. Also handles simple ${variables}, replacing them from the keys to $self.
  719. =cut
  720. sub eval_perl {
  721. my $self = shift;
  722. while ($_[0] =~ m{<perl>(.*?)</perl>}s) {
  723. my $block = $1;
  724. # Run the $block, and include anything returned -
  725. # or an error message, if we got one.
  726. my $output = eval $block;
  727. $output = $@ if $@;
  728. $_[0] =~ s{<perl>\Q$block\E</perl>}{$output}s;
  729. }
  730. # Interpolate variables:
  731. $_[0] =~ s/\${([a-zA-Z_]+)}/$self->{$1}/ge;
  732. return;
  733. }
  734. =item month_name
  735. Turn numeric dates into English.
  736. =cut
  737. sub month_name {
  738. my ($number) = @_;
  739. # "Null" is here so that $month_name[1] corresponds to January, etc.
  740. my @months = qw(Null January February March April May June
  741. July August September October November December);
  742. return $months[$number];
  743. }
  744. =item root_locations($file)
  745. Given a file/entry, return the appropriate concatenations with
  746. root_dir and url_root.
  747. =cut
  748. sub root_locations {
  749. return (
  750. $_[0]->local_path($_[1]),
  751. $_[0]->url_root . $_[1]
  752. );
  753. }
  754. =item local_path
  755. Return an absolute path for a given file. Called by root_locations.
  756. Arguably this is stupid and inefficient.
  757. =cut
  758. sub local_path {
  759. return $_[0]->root_dir . '/' . $_[1];
  760. }
  761. =item feed_print
  762. Return an Atom feed of entries for a month. Defaults to the most
  763. recent month in the archive.
  764. Called from handle(), requires XML::Atom::SimpleFeed.
  765. =cut
  766. sub feed_print {
  767. my $self = shift;
  768. my ($month) = @_;
  769. $month ||= $self->recent_month();
  770. my $feed_url = $self->url_root . $self->feed_alias;
  771. my ($month_file, $month_url) = $self->root_locations($month);
  772. my $feed = XML::Atom::SimpleFeed->new(
  773. title => $self->title,
  774. link => $self->url_root,
  775. link => { rel => 'self', href => $feed_url, },
  776. icon => $self->favicon_url,
  777. author => $self->author,
  778. id => $self->url_root,
  779. generator => 'Display.pm / XML::Atom::SimpleFeed',
  780. );
  781. my @entry_files;
  782. if (-d $month_file) {
  783. @entry_files = dir_list ($month_file,
  784. 'high_to_low',
  785. qr/^[0-9]{1,2}$/);
  786. } else {
  787. return 0;
  788. }
  789. foreach my $entry_file (@entry_files) {
  790. my $entry = "$month/$entry_file";
  791. my $entry_url = $month_url . "/$entry_file";
  792. $feed->add_entry(
  793. title => $entry,
  794. link => $entry_url,
  795. id => $entry_url,
  796. content => $self->entry($entry),
  797. );
  798. }
  799. return "Content-type: application/atom+xml\n\n"
  800. . $feed->as_string;
  801. }
  802. =back
  803. =head1 SEE ALSO
  804. walawiki.org, Blosxom, rassmalog, Text::Textile, XML::Atom::SimpleFeed,
  805. Image::Size, CGI::Fast.
  806. =head1 AUTHOR
  807. Copyright 2001-2007 Brennen Bearnes
  808. Image sizing code (in image_size) derived from wwwis, by Alex Knowles and
  809. Andrew Tong.
  810. display.pl is free software; you can redistribute it and/or modify
  811. it under the terms of the GNU General Public License as published by
  812. the Free Software Foundation; either version 2 of the License, or
  813. (at your option) any later version.
  814. This program is distributed in the hope that it will be useful,
  815. but WITHOUT ANY WARRANTY; without even the implied warranty of
  816. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  817. GNU General Public License for more details.
  818. =cut
  819. 1;