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.

1036 lines
30 KiB

  1. #!/usr/bin/perl
  2. =pod
  3. =head1 NAME
  4. display - script to display fragments of text on the web and elsewhere
  5. =head1 DESCRIPTION
  6. display started life as a way to concatenate fragments of handwritten HTML by
  7. date. While the script has since haphazardly accumulated several of the usual
  8. weblog features (comments, lightweight markup, feed generation, embedded Perl,
  9. poetry tools, stupid dependencies), it hasn't changed much in six years. The
  10. current version is intended to support FastCGI via CGI::Fast, if available, and
  11. otherwise operate as a traditional CGI or commandline utility. It may have some
  12. distance to go towards this goal.
  13. Entries are stored in a simple directory tree under C<$DISPLAY_CONF{ROOT_DIR}>.
  14. Like:
  15. archives/2001/1/1
  16. archives/2001/1/1/sub_entry
  17. An entry may be either a plain text file, or a directory containing several
  18. such files + whatever else you'd like to store. If it's a directory, the file
  19. called "index" will be treated as the text of the entry, and all other lower
  20. case filenames without extensions will be treated as sub-entries or documents
  21. within that entry, and displayed accordingly.
  22. Directories may be nested to an arbitrary depth, though I don't promise that
  23. this won't break on you.
  24. A PNG or JPEG file with a name like
  25. 2001/1/1.icon.png
  26. 2001/1/1/index.icon.png
  27. 2001/1/1/whatever.icon.png
  28. will be treated as an icon for the appropriate entry file.
  29. =head2 MARKUP
  30. Entries may consist of hand-written HTML (to be passed along without further
  31. interpretation), a supported form of lightweight markup, or some combination
  32. thereof. Actually, an entry may consist of any darn thing you please, as long
  33. as Perl will agree that it is text, but presumably you're going to be feeding
  34. this to a browser.
  35. Special markup is indicated by a variety of XML-style container tags.
  36. B<Embedded Perl> - evaluated and replaced by whatever value you return
  37. (evaluated in a scalar context):
  38. <perl>my $dog = "Ralph."; return $dog;</perl>
  39. This code is evaluated before any other processing is done, so you can return
  40. any other markup understood by the script and have it handled appropriately.
  41. B<Interpolated variables> - actually keys to %TEMPLATE, for the moment:
  42. <perl>$TEMPLATE{dog} = "Ralph"; return '';</perl>
  43. <p>My dog is named ${dog}.</p>
  44. Embedded code and variables are mostly intended for use in F<header> and
  45. F<footer> files, where it's handy to drop in titles or conditionalize aspects
  46. of a layout. You want to be careful with this sort of thing - it's useful in
  47. small doses, but it's also a maintainability nightmare waiting to happen.
  48. (WordPress, I am looking at you.)
  49. B<Several forms of lightweight markup>:
  50. <wala>Wala::Markup, via Wala.pm - very basic wiki syntax</wala>
  51. <textile>Dean Allen's Textile, via Brad Choate's
  52. Text::Textile.</textile>
  53. <freeverse>An easy way to
  54. get properly broken lines
  55. -- en and em dashes ---
  56. for poetry and such.</freeverse>
  57. B<And a couple of shortcuts>:
  58. <image>filename.ext
  59. alt text, if any</image>
  60. <list>
  61. one list item
  62. another list item
  63. </list>
  64. As it stands, freeverse, image, and list are not particularly robust.
  65. =cut
  66. use strict;
  67. use warnings;
  68. no warnings 'uninitialized';
  69. use lib 'lib';
  70. use lib 'wala';
  71. use CGI::Fast;
  72. use Image::Size;
  73. use Text::Textile;
  74. use XML::Atom::SimpleFeed;
  75. use Wala qw (%WalaConf %DISPLAY_CONF);
  76. # default options:
  77. %DISPLAY_CONF = (
  78. ROOT_DIR => 'archives', # root dir for archived files
  79. URL_ROOT => 'http://p1k3.com/', # root URL for building links
  80. IMAGE_URL_ROOT => 'http://p1k3.com/', # same for images
  81. HEADER => 'header',
  82. FOOTER => 'footer',
  83. );
  84. # Grab configuration:
  85. if (-e 'conf.pl') {
  86. do 'conf.pl';
  87. }
  88. $WalaConf{'ShowSearchlinks'} = 0;
  89. # Handle input from FastCGI:
  90. while (my $query = new CGI::Fast) {
  91. handle($query);
  92. }
  93. # Fini.
  94. =head1 SUBROUTINES
  95. For no bigger than this thing is, it gets a little convoluted.
  96. =over
  97. =item handle()
  98. Handle queries.
  99. =cut
  100. sub handle {
  101. my ($query) = @_;
  102. # Get the time, format the couple of variables I'll actually use.
  103. my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday,
  104. $isdst) = localtime(time);
  105. $mon++;
  106. $year += 1900;
  107. # grab the command line options, using "new" if none are provided
  108. #my @options = @ARGV;
  109. #unless ($options[0]) { $options[0] = $ENV{'QUERY_STRING'} };
  110. my @options = $query->param('keywords');
  111. unless ($options[0]) { $options[0] = 'new' };
  112. # now that we have some metadata,
  113. # set some variables to be used in fragment interpretation.
  114. # (these get inserted down in line_parse(); this is less than ideal)
  115. $DISPLAY_CONF{title} = join(' ', @options);
  116. # Unless this is already in an HTML document, spit out some default HTML.
  117. my $print_footer = 0;
  118. unless ($options[0] eq 'feed') {
  119. print fragment_slurp($DISPLAY_CONF{HEADER});
  120. $print_footer = 1;
  121. }
  122. # take care of "all" alias in options
  123. # get everything in the archive root directory
  124. my @old_options = @options;
  125. for (@old_options) {
  126. if ($_ eq 'all') {
  127. push (@options, dir_list ($DISPLAY_CONF{ROOT_DIR},
  128. "high_to_low",
  129. "^[0-9]{1,4}\$") );
  130. }
  131. }
  132. # do appropriate things:
  133. foreach my $option (@options) {
  134. # take care of trailing slashes
  135. chop ($option) if (substr($option, -1, 1) eq '/');
  136. # This just provides an alias for the most recent month.
  137. if ($option =~ m/^(feed|new)/) {
  138. my $special = $1;
  139. if (-e "$DISPLAY_CONF{ROOT_DIR}/$year/$mon") {
  140. $option = "$year/$mon";
  141. } else {
  142. $option = recent_month();
  143. }
  144. # Handle feed generation using XML::Atom::SimpleFeed.
  145. if ($special eq 'feed') {
  146. feed_print($option);
  147. exit;
  148. }
  149. }
  150. if ( $option =~ m'^[0-9/]{5,11}[a-z_/]+$' ) {
  151. # nnnn/[nn/nn/]doc_name
  152. # It's a document within a date. entry_print it.
  153. print entry_markup(entry_print($option, 'index') . datestamp($option));
  154. }
  155. elsif ( $option =~ m'^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}$' ) {
  156. # nnnn/nn/nn
  157. # It's a specific date. Print it in full.
  158. print entry_markup(entry_print($option, 'all') . datestamp($option));
  159. }
  160. elsif ( $option =~ m'^[0-9]{4}/[0-9]{1,2}$' ) {
  161. # nnnn/nn - It's a month. Print it.
  162. month_print($option);
  163. }
  164. elsif ( $option =~ m'^[0-9]{4}$' ) {
  165. # nnnn - It's a year. Display a list of entries.
  166. year_print($option);
  167. }
  168. elsif ($option =~ m'^[a-z_]') {
  169. # assume it's a document in the root directory
  170. print entry_markup(entry_print($option, 'all'));
  171. }
  172. }
  173. # Finish up...
  174. # Print a footer.
  175. if ($print_footer) {
  176. print fragment_slurp($DISPLAY_CONF{FOOTER});
  177. }
  178. }
  179. sub recent_month {
  180. my (@year_files) = dir_list ("$DISPLAY_CONF{ROOT_DIR}",
  181. 'high_to_low',
  182. '^[0-9]{1,4}$');
  183. my (@month_files) = dir_list ("$DISPLAY_CONF{ROOT_DIR}/$year_files[0]",
  184. 'high_to_low',
  185. '^[0-9]{1,2}');
  186. return "$year_files[0]/$month_files[0]";
  187. }
  188. =item dir_list()
  189. Return a $sort_order sorted list of files matching $pattern in a
  190. directory. Called by year_print(), month_print(), and entry_print().
  191. calls $sort_order, which can be one of
  192. alpha - alphabetical
  193. reverse_alpha - alphabetical, reversed (might not work yet)
  194. high_to_low - numeric, high to low
  195. low_to_high - numeric, low to high
  196. =cut
  197. sub dir_list {
  198. my ($dir, $sort_order, $file_pattern) = @_;
  199. my (@files);
  200. $file_pattern = "^[0-9]{1,2}\$" unless ($file_pattern);
  201. $sort_order = "high_to_low" unless ($sort_order);
  202. opendir LIST_DIR, $dir;
  203. @files = grep /$file_pattern/, readdir LIST_DIR;
  204. closedir LIST_DIR;
  205. @files = sort $sort_order @files;
  206. return @files;
  207. }
  208. # various named sorts for dir_list
  209. sub alpha { $a cmp $b; } # alphabetical
  210. sub high_to_low { $b <=> $a; } # numeric, high to low
  211. sub low_to_high { $a <=> $b; } # numberic, low to high
  212. sub reverse_alpha { $b cmp $a; } # alphabetical, reversed
  213. =item year_print()
  214. List out the updates for a year. Calls dir_list(), entry_print().
  215. =cut
  216. sub year_print {
  217. my ($year) = @_;
  218. my (@update_files, $update_count, $ico_markup);
  219. if (-d "$DISPLAY_CONF{ROOT_DIR}/$year") {
  220. print '<div class="entry">' . "\n";
  221. if (-T "$DISPLAY_CONF{ROOT_DIR}/$year/index") {
  222. print entry_print($year, 'index');
  223. }
  224. if ( $ico_markup = icon_markup($year, $year) ) {
  225. print "<h3>$ico_markup $year</h3>";
  226. } else {
  227. print "<h3>$year</h3>\n";
  228. }
  229. my @month_files = dir_list ("$DISPLAY_CONF{ROOT_DIR}/$year",
  230. "high_to_low",
  231. "^[0-9]{1,2}\$");
  232. print "\n<table>\n";
  233. $update_count = 0;
  234. foreach my $month_file (@month_files) {
  235. @update_files = dir_list ("$DISPLAY_CONF{ROOT_DIR}/$year/$month_file",
  236. "low_to_high", "^[0-9]{1,2}\$");
  237. # Add the count of files to $update_count.
  238. $update_count += @update_files;
  239. print '<tr> <td class="datelink">'
  240. . a("$DISPLAY_CONF{URL_ROOT}$year/$month_file", month_name($month_file))
  241. . "</td> <td class=\"datelink\">\n";
  242. print "( <small>";
  243. foreach my $update_file (@update_files) {
  244. print a("$DISPLAY_CONF{URL_ROOT}$year/$month_file/$update_file", $update_file)
  245. . "\n";
  246. }
  247. print "</small> )</td> </tr>\n\n";
  248. }
  249. print "</table>\n";
  250. print "<p>$update_count ";
  251. if ($update_count > 1) {
  252. my ($monthly_average) = int($update_count / @month_files);
  253. print " entries, an arithmetic mean of $monthly_average a month.";
  254. } elsif ($update_count == 0) {
  255. print " entries";
  256. } elsif ($update_count == 1) {
  257. print " entry";
  258. }
  259. print '</p>';
  260. } elsif (-T "$DISPLAY_CONF{ROOT_DIR}/$year") {
  261. print entry_print($year, 'index');
  262. } else {
  263. print '<p>No such year.</p>';
  264. }
  265. print "</div>\n";
  266. return ($update_count);
  267. }
  268. =item month_print()
  269. Prints the entries in a given month (nnnn/nn). Calls dir_list(), datestamp().
  270. =cut
  271. sub month_print {
  272. my ($year_digits, $month_digits, $calendar);
  273. # If a directory exists for $month, use dir_list to grab
  274. # the entry files it contains into @entry_files, sorted
  275. # numerically. Then send each entry to entry_print.
  276. my ($month) = @_;
  277. if (-d "$DISPLAY_CONF{ROOT_DIR}/$month") {
  278. if (-T "$DISPLAY_CONF{ROOT_DIR}/$month/index") {
  279. print entry_print($month, "index");
  280. }
  281. my (@entry_files) = dir_list ("$DISPLAY_CONF{ROOT_DIR}/$month",
  282. "high_to_low",
  283. "^[0-9]{1,2}\$");
  284. foreach my $entry_file (@entry_files) {
  285. print entry_markup( entry_print("$month/$entry_file", 'index')
  286. . datestamp("$month/$entry_file") );
  287. }
  288. } elsif (-T "$DISPLAY_CONF{ROOT_DIR}/$month") {
  289. print entry_print($month, 'index');
  290. }
  291. }
  292. =item entry_print()
  293. Prints the contents of a given entry. Calls datestamp, fragment_print,
  294. dir_list, and icon_markup. Recursively calls itself.
  295. =cut
  296. sub entry_print {
  297. my ($entry, $level) = @_;
  298. my ($result);
  299. my $entry_loc = "$DISPLAY_CONF{ROOT_DIR}/$entry"; # location of entry on local filesystem
  300. my $entry_url = $DISPLAY_CONF{URL_ROOT} . $entry; # and its URL
  301. # display an icon, if we have one.
  302. if ( my $ico_markup = icon_markup ($entry, "") ) {
  303. $result .= "<h2>$ico_markup</h2>\n\n";
  304. }
  305. if (-T $entry_loc) {
  306. # is text, slurp it and return
  307. return ($result . fragment_slurp($entry_loc));
  308. } elsif (-d $entry_loc) {
  309. # print index as head
  310. $result .= fragment_slurp ("$entry_loc/index");
  311. my @sub_entries = dir_list ($entry_loc, 'alpha',
  312. '^[a-z_]+(\.tgz|\.zip|\.tar\.gz)?$');
  313. # followed by any sub-entries
  314. if ( ($level eq 'index') and (@sub_entries > 1) ) {
  315. # if we're just supposed to print an index
  316. # spit out icons or text links for extra files
  317. my $contents;
  318. my %ignore_entries = ("index" => 1, "standing_bear" => 1);
  319. foreach my $sub_entry (@sub_entries) {
  320. next if ($ignore_entries{$sub_entry});
  321. if ( my $sub_ico_markup = icon_markup("$entry/$sub_entry",
  322. $sub_entry) ) {
  323. $contents .= qq|<a href="$DISPLAY_CONF{URL_ROOT}$entry/$sub_entry" |
  324. . qq|title="$sub_entry">$sub_ico_markup</a>\n |;
  325. } else {
  326. $contents .= qq|<a href="$DISPLAY_CONF{URL_ROOT}$entry/$sub_entry "|
  327. . qq|title="$sub_entry">$sub_entry</a> \n|;
  328. }
  329. }
  330. $result .= "<p><em><strong>more</strong></em>: $contents</p>\n";
  331. } elsif ( ($level eq 'all') and (@sub_entries > 1) ) {
  332. # but if we're supposed to print everything in the directory
  333. # and if there's more there than just the index file,
  334. foreach my $sub_entry (@sub_entries) {
  335. next if ($sub_entry eq 'index'); # skip index
  336. # print each of the other files, separated by little headers
  337. $result .= "\n\n<p class=\"centerpiece\">{"
  338. . a("$DISPLAY_CONF{URL_ROOT}$entry/$sub_entry", $sub_entry)
  339. . "}</p>\n\n";
  340. # skipping any archives
  341. next if ($sub_entry =~ m/(\.tgz|\.zip|\.tar\.gz)$/);
  342. $result .= entry_print("$entry/$sub_entry", 'index');
  343. }
  344. }
  345. }
  346. return $result;
  347. }
  348. =item icon_markup()
  349. Check if an icon exists for a given entry if so, return markup to include it.
  350. Icons are PNG or JPEG image files following a specific naming convention:
  351. index.icon.[png|jp(e)g] for directories
  352. [filename].icon.[png|jp(e)g] for flat text files
  353. Called by entry_print, calls image_size, uses filename to determine type.
  354. =cut
  355. sub icon_markup {
  356. my ($entry, $alt) = @_;
  357. my ($entry_loc) = "$DISPLAY_CONF{ROOT_DIR}/$entry";
  358. my ($entry_url) = "$DISPLAY_CONF{IMAGE_URL_ROOT}${entry}";
  359. my ($icon_loc, $icon_url);
  360. if (-T $entry_loc) {
  361. $icon_loc = "$entry_loc.icon";
  362. $icon_url = "$entry_url.icon";
  363. } elsif (-d $entry_loc) {
  364. $icon_loc = "$entry_loc/index.icon";
  365. $icon_url = "$entry_url/index.icon";
  366. }
  367. # put a list of icon image types to check for here
  368. # (first one found will be used)
  369. my (@suffixes) = qw(png jpg jpeg);
  370. my $suffix = "";
  371. for (@suffixes) {
  372. if (-e "$icon_loc.$_") {
  373. $suffix = $_;
  374. last;
  375. }
  376. }
  377. # fail unless there's a file with one of the above suffixes
  378. return 0 unless $suffix;
  379. # call image_size to slurp width & height from the image file
  380. my ($width, $height) = image_size("$icon_loc.$suffix");
  381. return qq|<img src="$icon_url.$suffix"\n width="$width" |
  382. . qq|height="$height"\n alt="$alt" />|;
  383. }
  384. =item datestamp()
  385. Returns a nice html datestamp for a given entry, including a wikilink for
  386. discussion and suchlike. Called by entry_print.
  387. =cut
  388. sub datestamp {
  389. my ($entry, $markup_start, $markup_end) = @_;
  390. unless ($markup_start and $markup_end) {
  391. $markup_start = "\n<p class=\"datelink\">";
  392. $markup_end = "</p>\n\n";
  393. }
  394. my ($stamp);
  395. if ( $entry =~ m/(^[0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/ ) {
  396. my ($entry_year, $entry_month, $entry_day) = split (/\//, $1);
  397. # this stuff conditionalizes the wikilink
  398. # so that if nothing exists, you wind up with an edit form
  399. my ($wiki_date_name) = month_name($entry_month) . "_${entry_day}_${entry_year}";
  400. my $wikistamp;
  401. if (-e "$WalaConf{PagesDir}/${wiki_date_name}") {
  402. $wikistamp = qq{:: <a title="a page you can edit"}
  403. . qq{ href="$WalaConf{ScriptName}?$wiki_date_name">read the margins</a>};
  404. } else {
  405. $wikistamp = qq{:: <a title="a page you can edit"}
  406. . qq{ href="$WalaConf{ScriptName}?$wiki_date_name">write in the margins</a>};
  407. }
  408. # return a fancy datestamp.
  409. my $month_name = month_name($entry_month);
  410. $stamp = <<STAMP;
  411. $markup_start
  412. <a href="$DISPLAY_CONF{URL_ROOT}$entry_year" title="$entry_year">$entry_year</a>
  413. <a href="$DISPLAY_CONF{URL_ROOT}$entry_year/$entry_month" title="$entry_year/$entry_month">$month_name</a>
  414. <a href="$DISPLAY_CONF{URL_ROOT}$entry_year/$entry_month/$entry_day" title="$entry_year/$entry_month/$entry_day">$entry_day</a>
  415. $wikistamp
  416. $markup_end
  417. STAMP
  418. } else {
  419. $stamp = "$markup_start(failed to construct datestamp for $entry)$markup_end";
  420. }
  421. return ($stamp);
  422. }
  423. =item fragment_print()
  424. Print a text fragment - a header, footer, update, etc.
  425. Called by main routines, used to print headers and footers.
  426. Calls fragment_slurp to get the fragment it's supposed to print.
  427. Returns 1 on successful completion, 0 otherwise.
  428. =cut
  429. sub fragment_print {
  430. my ($file) = @_;
  431. my $lines = fragment_slurp($file);
  432. if (length($lines)) {
  433. print $lines;
  434. } else {
  435. return '';
  436. }
  437. }
  438. =item fragment_slurp()
  439. Read a text fragment, call line_parse to take care of funky markup and
  440. interpreting embedded code, and then return it as a string. Takes one
  441. parameter, the name of the file, and returns '' if it's not an extant text
  442. file. Called by entry_print, at least
  443. =cut
  444. sub fragment_slurp {
  445. my ($file) = @_;
  446. # if $file is text
  447. if (-T $file) {
  448. my $everything;
  449. open (my $fh, '<', $file) or return '';
  450. {
  451. # line sep
  452. local $/ = undef;
  453. $everything = <$fh>;
  454. }
  455. close $fh;
  456. # take care of any special markup
  457. # we feed $file to line_parse so it has some context to work with
  458. $everything = line_parse ($file, $everything);
  459. return $everything;
  460. } else {
  461. return '';
  462. }
  463. }
  464. =item line_parse()
  465. Performs substitutions on lines called by fragment_slurp, at least. Calls
  466. image_markup, Text::Textile, Wala::wiki_page_to_html, eval_perl. Returns
  467. string.
  468. Parses some special markup, specifically:
  469. <perl>embedded perl</perl>
  470. ${variable} interpolation from %DISPLAY_CONF
  471. <textile></textile> - Text::Textile to HTML
  472. <wala></wala> - Wala::wikify();
  473. <image>filename.ext</image>
  474. <freeverse></freeverse>
  475. <retcon></retcon>
  476. <list></list>
  477. =cut
  478. sub line_parse {
  479. my ($file, $everything) = (@_);
  480. # eval embedded Perl
  481. $everything = eval_perl($everything, $file);
  482. # interpolate variables
  483. $everything =~ s/\${([a-zA-Z_]+)}/$DISPLAY_CONF{$1}/ge;
  484. # take care of wala markup
  485. $everything =~ s/<wala>(.*?)<\/wala>/Wala::wikify($1)/seg;
  486. # take care of textile markup, if we've got any
  487. # this is wrapped in a conditional to keep from
  488. # creating the object if we don't need it.
  489. if ($everything =~ m/<textile>/s) {
  490. # head_offset: use h1., h2. in Textile formatting.
  491. my $textile = Text::Textile->new( head_offset => 2 );
  492. $everything =~ s/<textile>(.*?)<\/textile>/$textile->process($1)/seg;
  493. }
  494. # evaluate <image> tags.
  495. $everything =~ s!<image>(.*?)</image>!image_markup($file, $1)!seg;
  496. my %tags = ( retcon => 'div class="retcon"',
  497. freeverse => 'p',
  498. list => "ul>\n<li" );
  499. my %end_tags = ( retcon => 'div',
  500. freeverse => 'p',
  501. list => 'li></ul' );
  502. my %blank_lines = ( freeverse => "</p>\n\n<p>",
  503. list => "</li>\n\n<li>" );
  504. my %newlines = ( freeverse => "<br />\n" );
  505. my %dashes = ( freeverse => ' &mdash; ' );
  506. foreach my $key (keys %tags) {
  507. # Set some replacements, unless they've been explicitly set already.
  508. $end_tags{$key} = $tags{$key} unless $end_tags{$key};
  509. $blank_lines{$key} = "\n\n" unless $blank_lines{$key};
  510. $newlines{$key} = "\n" unless $newlines{$key};
  511. $dashes{$key} = " -- " unless $dashes{$key};
  512. while ($everything =~ m/(<$key>.*?<\/$key>)/s) {
  513. my $block = $1;
  514. # save the bits between instances of the block --
  515. # the \Q and \E escape any regex chars in the block
  516. my (@interstice_array) = split (/\Q$block\E/s, $everything);
  517. # now, transform the contents of the block we've found:
  518. # tags that surround the block
  519. $block =~ s/\n?<$key>\n?/<$tags{$key}>/gs;
  520. $block =~ s!\n?</$key>\n?!</$end_tags{$key}>!gs;
  521. # dashes
  522. $block =~ s/(\s+)\-{2}(\s+)/$1$dashes{$key}$2/gs;
  523. # blank lines within the block
  524. $block =~ s/\n\n/$blank_lines{$key}/gs;
  525. # single newlines (i.e., line ends) within the block
  526. # except those preceded by a double-quote, which probably
  527. # indicates a still-open tag.
  528. $block =~ s/([^"\n])\n([^\n])/$1$newlines{$key}$2/gs;
  529. # and slap it all back together as $everything
  530. $everything = join $block, @interstice_array;
  531. }
  532. }
  533. return $everything;
  534. }
  535. =item eval_perl()
  536. Evaluate embedded Perl, replacing blocks enclosed with <perl> tags
  537. with whatever they return (well, evaluated in a scalar context).
  538. =cut
  539. sub eval_perl {
  540. my ($everything, $file) = @_;
  541. while ($everything =~ m/<perl>(.*?)<\/perl>/s) {
  542. my $block = $1;
  543. my $output = eval $block;
  544. if ($@) {
  545. # got an error
  546. $everything =~ s/<perl>\Q$block\E<\/perl>/$@ in $file/s;
  547. } else {
  548. # include anything returned from $block
  549. $everything =~ s/<perl>\Q$block\E<\/perl>/$output/s;
  550. }
  551. }
  552. return $everything;
  553. }
  554. =item image markup()
  555. Parse out an image tag and return the appropriate html. Calls image_size.
  556. Called by line_parse.
  557. =cut
  558. sub image_markup {
  559. my ($file, $block) = @_;
  560. # get a directory for the file we're working with
  561. $file =~ s'[^/]* # everything not a /
  562. $ # up to end of string
  563. ''x;
  564. # truncated file date that just includes date + sub docs
  565. my ($file_date) = $file =~ m'([0-9]{4}/[0-9]{1,2}/[0-9]{1,2}/([a-z]*/)*)$';
  566. my ($image_name, $alt_text) = split/\n/, $block;
  567. my $image_file;
  568. if (-e "$file/$image_name" ) {
  569. $image_file = "$file/$image_name";
  570. $image_name = "${file_date}${image_name}";
  571. } elsif (-e "$DISPLAY_CONF{ROOT_DIR}/$image_name") {
  572. $image_file = "$DISPLAY_CONF{ROOT_DIR}/$image_name";
  573. }
  574. # get width & height in pixels for known filetypes
  575. my ($width, $height) = image_size($image_file);
  576. # may need to change this if rewrites don't work
  577. return qq|<img src="$DISPLAY_CONF{IMAGE_URL_ROOT}$image_name"\n height="$height"|
  578. . qq|\n width="$width"\n alt="$alt_text" />|;
  579. }
  580. =item month_name()
  581. Turn numeric dates into English.
  582. =cut
  583. sub month_name {
  584. my ($number) = @_;
  585. # "Null" is here so that $month_name[1] corresponds to January, etc.
  586. my @months = qw(Null January February March April May June
  587. July August September October November December);
  588. return $months[$number];
  589. }
  590. =item feed_print()
  591. Dump out an Atom feed of entries for a month.
  592. Called from handle(), calls entry_print, requires XML::Atom::SimpleFeed.
  593. =cut
  594. sub feed_print {
  595. my $month = shift;
  596. # create a feed object
  597. my $feed = XML::Atom::SimpleFeed->new(
  598. title => $DISPLAY_CONF{title},
  599. link => $DISPLAY_CONF{URL_ROOT},
  600. link => { rel => 'self', href => $DISPLAY_CONF{feed_url}, },
  601. icon => $DISPLAY_CONF{favicon_url},
  602. author => $DISPLAY_CONF{author},
  603. id => $DISPLAY_CONF{URL_ROOT},
  604. );
  605. # If a directory exists for $month, use dir_list to grab
  606. # the entry files it contains into @entry_files, sorted
  607. # numerically. Then send each entry to entry_print.
  608. my @entry_files;
  609. if (-d "$DISPLAY_CONF{ROOT_DIR}/$month") {
  610. (@entry_files) = dir_list ("$DISPLAY_CONF{ROOT_DIR}/$month", 'high_to_low',
  611. "^[0-9]{1,2}\$");
  612. } else {
  613. return 0;
  614. }
  615. foreach my $entry_file (@entry_files) {
  616. # Going to feed this to SimpleFeed.
  617. my $content = entry_print("$month/$entry_file", 'index');
  618. $feed->add_entry(
  619. title => "$month/$entry_file",
  620. link => $DISPLAY_CONF{URL_ROOT} . "$month/$entry_file",
  621. id => $DISPLAY_CONF{URL_ROOT} . "$month/$entry_file",
  622. content => $content,
  623. );
  624. }
  625. print "Content-type: application/atom+xml\n\n";
  626. $feed->print;
  627. return 1;
  628. }
  629. =item entry_markup()
  630. Return text wrapped in the appropriate markup for an entry. Just a wrapper
  631. around div() at the moment.
  632. =cut
  633. sub entry_markup {
  634. my ($text) = @_;
  635. return div($text, 'entry');
  636. }
  637. =item div()
  638. Return text wrapped in a div of the specified class.
  639. =cut
  640. sub div {
  641. my ($text, $class) = @_;
  642. my ($top, $result);
  643. if ($class) {
  644. $top = "<div class=\"$class\">\n";
  645. } else {
  646. $top = "<div>\n";
  647. }
  648. return ( $top . $text . "\n</div>\n" );
  649. }
  650. =item a()
  651. Returns an HTML link. Called all over the place.
  652. =cut
  653. sub a {
  654. my ($url, $text) = @_;
  655. return "<a href=\"$url\">$text</a>";
  656. }
  657. =item ornament()
  658. Returns a type ornament.
  659. =cut
  660. sub ornament {
  661. return '<small>&sect;</small>';
  662. }
  663. =item image_size()
  664. Returns (width, height) of a variety of image files. Called by icon_markup and
  665. line_parse. Uses Image::Size if available, otherwise uses a couple of built-in
  666. routines munged together from pngsize and jpegsize in wwwis, by Alex Knowles
  667. and Andrew Tong.
  668. =cut
  669. sub image_size {
  670. my ($image_file) = shift;
  671. # Use Image::Size - this needs to be actually conditionalized.
  672. my ($x, $y, $type);
  673. ($x, $y, $type) = imgsize($image_file);
  674. return ($x, $y);
  675. # Otherwise we want to use our built-in routines:
  676. my ($head);
  677. if ( !open(IMAGE, '<', $image_file) ) {
  678. print STDERR "can't open IMG $image_file";
  679. return (0, 0);
  680. } else {
  681. binmode IMAGE;
  682. if ($image_file =~ m/\.png$/) { # it's a PNG
  683. my ($a, $b, $c, $d, $e, $f, $g, $h) = 0;
  684. if (defined($image_file)
  685. && read(IMAGE, $head, 8) == 8
  686. && ($head eq "\x8a\x4d\x4e\x47\x0d\x0a\x1a\x0a" ||
  687. $head eq "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a")
  688. && read(IMAGE, $head, 4) == 4
  689. && read(IMAGE, $head, 4) == 4
  690. && ($head eq "MHDR" || $head eq "IHDR")
  691. && read(IMAGE, $head, 8) == 8) {
  692. # ($x, $y) = unpack("I"x2, $head);
  693. # doesn't work on little-endian machines
  694. # return ($x,$y);
  695. ($a,$b,$c,$d,$e,$f,$g,$h) = unpack ("C"x8, $head);
  696. return ($a<<24|$b<<16|$c<<8|$d, $e<<24|$f<<16|$g<<8|$h);
  697. }
  698. } elsif ($image_file =~ m/\.jpe?g$/) { # it's a JPEG
  699. my($done) = 0;
  700. my($c1,$c2,$ch,$s,$length, $dummy) = (0,0,0,0,0,0);
  701. my($a,$b,$c,$d);
  702. if (defined($image_file)
  703. && read(IMAGE, $c1, 1)
  704. && read(IMAGE, $c2, 1)
  705. && ord($c1) == 0xFF
  706. && ord($c2) == 0xD8) {
  707. while (ord($ch) != 0xDA && !$done) {
  708. # Find next marker (JPEG markers begin with 0xFF)
  709. # This can hang the program!!
  710. while (ord($ch) != 0xFF) {
  711. return(0,0) unless read(IMAGE, $ch, 1);
  712. }
  713. # JPEG markers can be padded with unlimited 0xFF's
  714. while (ord($ch) == 0xFF) {
  715. return(0,0) unless read(IMAGE, $ch, 1);
  716. }
  717. # Now, $ch contains the value of the marker.
  718. if ((ord($ch) >= 0xC0) && (ord($ch) <= 0xC3)) {
  719. return(0,0) unless read (IMAGE, $dummy, 3);
  720. return(0,0) unless read(IMAGE, $s, 4);
  721. ($a,$b,$c,$d)=unpack("C"x4,$s);
  722. return ($c<<8|$d, $a<<8|$b );
  723. } else {
  724. # We **MUST** skip variables, since FF's within
  725. # variable names are NOT valid JPEG markers
  726. return(0,0) unless read (IMAGE, $s, 2);
  727. ($c1, $c2) = unpack("C"x2,$s);
  728. $length = $c1<<8|$c2;
  729. last if (!defined($length) || $length < 2);
  730. read(IMAGE, $dummy, $length-2);
  731. }
  732. }
  733. }
  734. }
  735. return (0,0);
  736. }
  737. }
  738. =back
  739. =head1 SEE ALSO
  740. walawiki.org, Blosxom, rassmalog, Text::Textile, XML::Atom::SimpleFeed,
  741. Image::Size, CGI::Fast.
  742. =head1 AUTHOR
  743. Copyright 2001-2007 Brennen Bearnes
  744. Image sizing code (in image_size) derived from wwwis, by Alex Knowles and
  745. Andrew Tong.
  746. display.pl is free software; you can redistribute it and/or modify
  747. it under the terms of the GNU General Public License as published by
  748. the Free Software Foundation; either version 2 of the License, or
  749. (at your option) any later version.
  750. This program is distributed in the hope that it will be useful,
  751. but WITHOUT ANY WARRANTY; without even the implied warranty of
  752. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  753. GNU General Public License for more details.
  754. =cut
  755. 1;