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.

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