--- /dev/null
+# Blosxom Plugin: allconsuming -*- cperl -*-
+# Author: Todd Larason (jtl@molehill.org)
+# Version: 0+4i
+# Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
+# Netflix plugin Home/Docs/Licensing:
+# http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/AllConsuming/
+package allconsuming;
+
+# http://allconsuming.net/news/000012.html
+
+# -------------- Configuration Variables --------------
+# AllConsuming username
+$username = undef
+ unless defined $username;
+
+# Amazon Associate ID; feel free to leave this =)
+$associate_id = 'mtmolel-20'
+ unless defined $associate_id;
+
+# undef == "list all"
+# 0 == "don't list at all"
+# >0 == list first N (or all, if < N)
+# <0 == list random N (or all in random order, if < N)
+%num = (
+ purchased => 5, # most recent 5
+ reading => undef, # all
+ rereading => undef, # all
+ favorite => -5, # random 5
+ completed => 5, # most recent 5
+ nofinish => 0 # none
+ ) unless scalar keys %num > 0;
+
+# one of: SOAP::Lite, LWP, wget (or a pathname to wget), curl (or a pathname)
+# SOAP::Lite should be fastest and most likely to stay working long-term,
+# but is the hardest to get installed
+$networking = 'LWP'
+ unless defined $networking;
+
+# Whether to try to use caching; default is yes, and caching is very
+# strongly recommended
+$use_caching = 1
+ unless defined $use_caching;
+
+# how long to go between re-fetching the data, in seconds
+# default value is 1 week
+$max_cache_data_age = 60 * 60 * 24 * 7
+ unless defined $max_cache_data_age;
+
+# how long to go between re-formatting the lists, in seconds
+# default is 5 minutes
+$max_cache_layout_age = 60 * 5
+ unless defined $max_cache_layout_age;
+
+$debug_level = 1
+ unless defined $debug_level;
+# -----------------------------------------------------
+\f
+$purchased = '';
+$reading = '';
+$rereading = '';
+$favorite = '';
+$completed = '';
+$nofinish = '';
+\f
+use CGI qw/param/;
+use strict;
+use vars qw/$username $associate_id $max_cache_data_age $max_cache_layout_age
+ %num $networking $use_caching $debug_level
+ $purchased $reading $rereading $favorite $completed $nofinish/;
+\f
+my $cache;
+my $package = "allconsuming";
+my $cachefile = "$blosxom::plugin_state_dir/.$package.cache";
+my $save_cache = 0;
+\f
+# General utility functions
+
+sub debug {
+ my ($level, @msg) = @_;
+
+ if ($debug_level >= $level) {
+ print STDERR "$package debug $level: @msg\n";
+ }
+}
+
+sub load_template {
+ my ($bit) = @_;
+ return $blosxom::template->('', "$package.$bit", $blosxom::flavour);
+}
+
+sub report {
+ my ($bit, $listname, $title, $author, $asin, $image, $allconsuming_url, $amazon_url) = @_;
+ my $f = load_template("$listname.$bit") || load_template($bit);
+ $title = encode_entities($title);
+ $author = encode_entities($author);
+ $f =~ s/((\$[\w:]+)|(\$\{[\w:]+\}))/$1 . "||''"/gee;
+ return $f;
+}
+
+sub encode_entities {
+ my ($text) = @_;
+ eval "require HTML::Entities";
+ if ($@) {
+ my %map = ('<' => 'lt', '&' => 'amp', '>' => 'gt');
+ my $keys = join '',keys %map;
+ $text =~ s:([$keys]):&$map{$1};:g;
+ return $text;
+ }
+ return HTML::Entities::encode_entities($text);
+}
+\f
+# General networking
+
+sub GET {
+ my ($url) = @_;
+
+ if ($networking =~ m:curl:) {
+ return `$networking -m 30 -s "$url"`;
+ } elsif ($networking =~ m:wget:) {
+ return `$networking --quiet -O - "$url"`;
+ } elsif ($networking eq 'LWP') {
+ foreach (qw/LWP::UserAgent HTTP::Request::Common/) {
+ eval "require $_";
+ if ($@) {
+ debug(0, "Can't load $_, can't use LWP networking: $@");
+ return undef;
+ }
+ }
+ my $ua = LWP::UserAgent->new;
+ my $res = $ua->request(HTTP::Request::Common::GET $url);
+ if (!$res->is_success) {
+ my $error = $res->status_line;
+ debug(0, "HTTP GET error: $error");
+ return undef;
+ }
+ return $res->content;
+ } else {
+ debug(0, "ERROR: invalid \$networking $networking");
+ }
+}
+\f
+# AllConsuming-specific networking
+
+sub allconsuming_handle {
+ if ($networking eq 'SOAP::Lite') {
+ eval "require SOAP::Lite;";
+ if ($@) {
+ debug(0, "SOAP::Lite couldn't be loaded");
+ return undef;
+ }
+ my @now = localtime;
+ my $soap = SOAP::Lite
+ -> uri('http://www.allconsuming.net/AllConsumingAPI')
+ -> proxy('http://www.allconsuming.net/soap.cgi');
+ my $obj = $soap
+ -> call(new => $now[2], $now[3], $now[4] + 1, $now[5] + 1900)
+ -> result;
+ return {soap => $soap,
+ obj => $obj,
+ map => {purchased => 'GetPurchasedBooksList',
+ reading => 'GetCurrentlyReadingList',
+ rereading => 'GetRereadingBooksList',
+ favorite => 'GetFavoriteBooksList',
+ completed => 'GetCompletedBooksList',
+ nofinish => 'GetNeverFinishedBooksList'}
+ };
+ } else {
+ return {
+ map => {purchased => 'purchased_books',
+ reading => 'currently_reading',
+ rereading => 'rereading_books',
+ favorite => 'favorite_books',
+ completed => 'completed_books',
+ nofinish => 'never_finished_books'}
+ };
+ }
+}
+
+sub allconsuming_lookup {
+ my ($handle, $username, $list) = @_;
+
+ return undef unless defined $handle;
+
+ if ($networking eq 'SOAP::Lite') {
+ return undef unless defined $handle->{map}{$list};
+ return $handle->{soap}
+ -> call($handle->{map}{$list} => $handle->{obj}, $username)
+ -> result;
+ } else {
+ my $data = GET('http://allconsuming.net/soap-client.cgi?' .
+ "$handle->{map}{$list}=1&username=$username");
+ $data =~ s:\A\<pre>\$VAR1 =(.*)</pre>\Z:\1:ms;
+ return eval $data;
+ }
+}
+
+sub get_data {
+ if (defined $cache->{data} and
+ $^T - $cache->{data_timestamp} < $max_cache_data_age) {
+ return;
+ }
+ debug(1, "cache miss data");
+ $cache->{data_timestamp} = $^T;
+ my $obj = allconsuming_handle();
+
+ foreach (keys %num) {
+ next if defined($num{$_}) && $num{$_} == 0;
+ $cache->{data}{$_} = allconsuming_lookup($obj, $username, $_);
+ }
+ $save_cache = 1;
+}
+\f
+# Cache handling
+
+sub prime_cache {
+ return if (!$use_caching);
+ eval "require Storable";
+ if ($@) {
+ debug(1, "cache disabled, Storable not available");
+ $use_caching = 0;
+ return 0;
+ }
+ if (!Storable->can('lock_retrieve')) {
+ debug(1, "cache disabled, Storable::lock_retrieve not available");
+ $use_caching = 0;
+ return 0;
+ }
+ $cache = (-r $cachefile ? Storable::lock_retrieve($cachefile) : {});
+ if (defined(param('allconsuming'))) {
+ if (param('allconsuming') eq 'refresh_data') {
+ $cache = {};
+ } elsif (param('allconsuming') eq 'refresh_layout') {
+ $cache->{layout} = {};
+ }
+ }
+}
+
+sub save_cache {
+ return if (!$use_caching || !$save_cache);
+ debug(1, "Saving cache");
+ -d $blosxom::plugin_state_dir
+ or mkdir $blosxom::plugin_state_dir
+ or (debug(0, "State dir $blosxom::plugin_state_dir nonexistant and noncreatable: $!") and return);
+ Storable::lock_store($cache, $cachefile);
+}
+\f
+sub build_list {
+ my ($listname, $num, $list) = @_;
+ my $count = 0;
+ my $results;
+
+ return '' if (defined $num and $num == 0);
+ $list = [$list] if (ref $list eq 'HASH');
+ if (defined $list and defined $num and $num < 0) {
+ # algorithm from Algorithm::Numerical::Shuffle by Abigail
+ for (my $i = @$list; -- $i;) {
+ my $r = int rand ($i + 1);
+ ($list -> [$i], $list -> [$r]) = ($list -> [$r], $list -> [$i]);
+ }
+ $num = -$num;
+ }
+ $results = report('head', $listname);
+ foreach (@$list) {
+ $results .= report('item', $listname,
+ @{$_}{qw/title author asin image allconsuming_url
+ amazon_url/});
+ $count++;
+ last if (defined $num and $count == $num);
+ }
+ $results .= report('foot', $listname);
+
+ return $results;
+}
+\f
+# Blosxom plugin interface
+
+sub head {
+ prime_cache();
+ get_data();
+ save_cache();
+
+ foreach (keys %num) {
+ next if defined($num{$_}) && $num{$_} == 0;
+ no strict;
+ $$_ = $cache->{layout}{$_}{$blosxom::flavour};
+ next if (defined $$_ &&
+ ($^T - $cache->{layout_timestamp}{$_}{$blosxom::flavour}
+ < $max_cache_layout_age));
+ debug(1, "cache miss layout $_ $blosxom::flavour");
+ $$_ = build_list($_, $num{$_}, $cache->{data}{$_}{asins});
+ $cache->{layout}{$_}{$blosxom::flavour} = $$_;
+ $cache->{layout_timestamp}{$_}{$blosxom::flavour} = $^T;
+ $save_cache = 1;
+ use strict;
+ }
+ save_cache();
+
+ 1;
+}
+
+sub start {
+ return 0 unless defined $username;
+ while (<DATA>) {
+ last if /^(__END__)?$/;
+ chomp;
+ my ($flavour, $comp, $txt) = split ' ',$_,3;
+ $txt =~ s:\\n:\n:g;
+ $blosxom::template{$flavour}{"$package.$comp"} = $txt;
+ }
+ return 1;
+}
+
+1;
+__DATA__
+error head <table class="allconsuming $listname">\n
+error item <tr><td><a href="http://www.amazon.com/exec/obidos/ASIN/$asin/$associate_id/ref=nosim"><img border="0" src="$image" alt="$title Book cover"></a></td><td><a href="http://www.amazon.com/exec/obidos/ASIN/$asin/$associate_id/ref=nosim"><i>$title</i></a>, $author</td></tr>\n
+error foot </table>
+__END__
+
+=head1 NAME
+
+Blosxom Plug-in: allconsuming
+
+=head1 SYNOPSIS
+
+Purpose: Lets you easily share your AllConsuming information
+
+ * $allconsuming::purchased -- list of books you've purchased
+ * $allconsuming::reading -- list of books you're reading
+ * $allconsuming::rereading -- list of books you're re-reading
+ * $allconsuming::favorite -- list of your favorite books
+ * $allconsuming::completed -- list of books you've completed
+ * $allconsuming::nofinish -- list of books you never finished
+
+=head1 VERSION
+
+0+3i
+
+2nd test release
+
+=head1 AUTHOR
+
+Todd Larason <jtl@molehill.org>, http://molelog.molehill.org/
+
+=head1 BUGS
+
+None known; address bug reports and comments to me or to the Blosxom
+mailing list [http://www.yahoogroups.com/groups.blosxom].
+
+=head1 Customization
+
+=head2 Configuration variables
+
+C<$username> is your AllConsuming username. Until it's defined, this plugin does nothing.
+
+C<$associate_id> is an Amazon Associate ID. By default, it's mine;
+ change it to yours if you have one.
+
+C<%num> sets how many items to include in each list. Each of C<purchased>,
+C<reading>, C<rereading>, C<favorite>, C<completed> and C<nofinish> can be
+set separately. Setting C<$num{foo}> to undef means to include the whole
+list; setting it to 0 means to not build the list at all (or retrieve the
+data from AllConsuming); setting it to a positive number N means to list the
+first N items (or the whole list, if there aren't that many items) in order;
+setting it to a negative number -N means to list a randomly selected set of
+N items (or the whole list, in a random order, if there are fewer than N
+items).
+
+C<$networking> controls which networking implemenentation to use. If set to
+'SOAP::Lite', then the SOAP::Lite module will be used to communicate with
+AllConsuming's official SOAP interface; this method is preferable for both
+speed and reliability, but requires by far the most work to get working if
+you don't already have the modules installed. If set to 'LWP', then the
+LWP family of modules will be used to communicate with a demonstration CGI
+script. If set to 'wget' or 'curl' (or a pathname that includes one of
+those), then the respective external utility is used to communicate with
+the demonstration CGI script.
+
+C<$use_caching> is a boolean controlling whether to use caching at all.
+Caching is very strongly recommended -- AllConsuming is rather slow.
+
+C<$max_cache_data_age> sets how long to cache the downloaded AllConsuming
+information for. Fetching the data is pretty slow, so this defaults to a high
+value -- 1 week.
+
+C<$max_cache_layout_age> sets how long to cache the formatted data.
+Formatting the data is relatively fast, so this defaults to a small value -- 5
+minutes. If you aren't modifying templates and aren't using randomized lists,
+this can be set to the same as $max_cache_data_age without ill effects.
+
+C<$debug_level> can be set to a value between 0 and 5; 0 will output
+no debug information, while 5 will be very verbose. The default is 1,
+and should be changed after you've verified the plugin is working
+correctly.
+
+=head2 Classes for CSS control
+
+There's are some classes used, available for CSS customization.
+
+ * C<allconsuming> -- all lists are in the netflix class
+ * C<purchased>, etc -- each list is also in a class named for the list
+
+=head2 Flavour-style files
+
+If you want a format change that can't be made by CSS, you can
+override the HTML generated by creating files similar to Blosxom's
+flavour files. They should be named allconsuming.I<bit>.I<flavour>; for
+available I<bit>s and their default meanings, see the C<__DATA__>
+section in the plugin.
+
+=head1 Caching
+
+Because fetching the queue information is relatively slow, caching is very
+strongly recommended. Caching requires a version of the Storable module
+that supports the 'lock_save' and 'lock_retrieve' functions.
+
+Since the data reload is so slow, you may wish to raise the $max_cache_data_age
+even higher, and use explicit cache reloading. The cache can be reloaded
+either by deleting the cache file $plugin_state_dir/.allconsuming.cache, or
+by passing an C<allconsuming=refresh_data> parameter to the blosxom script;
+the latter is preferable, as you can insure that you take the time hit, not
+a random visitor.
+
+=head1 LICENSE
+
+this Blosxom Plug-in
+Copyright 2003, Todd Larason
+
+(This license is the same as Blosxom's)
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+=cut
--- /dev/null
+# Blosxom Plugin: autocorrect -*- perl -*-
+# Author: Todd Larason (jtl@molehill.org)
+# Version: 0+1i
+# Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
+# AutoCorrect plugin Home/Docs/Licensing:
+# http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/AutoCorrect/
+
+package autocorrect;
+
+# --- Configuration Variables ---
+
+$debug_level ||= 1;
+# -------------------------------
+
+
+use CGI;
+use FileHandle;
+
+my $package = 'autocorrect';
+my @goodhits = ();
+my $activated = 0;
+my %template = ();
+my $flav_cache;
+
+sub debug {
+ my ($level, @msg) = @_;
+
+ if ($debug_level >= $level) {
+ print STDERR "$package debug $level: @msg\n";
+ }
+}
+
+sub load_template {
+ my ($bit) = @_;
+ my $fh = new FileHandle;
+
+ return $flav_cache{$bit} ||=
+ ($fh->open("< $blosxom::datadir/$package.$bit.$blosxom::flavour") ?
+ join '',<$fh> : $template{$blosxom::flavour}{$bit}) ||
+ ($fh->open("< $blosxom::datadir/$package.$bit.$blosxom::default_flavour") ?
+ join '',<$fh> : $template{$blosxom::default_flavour}{$bit}) ||
+ ($fh->open("< $blosxom::datadir/$package.$bit.html") ?
+ join '',<$fh> : $template{html}{$bit}) ||
+ '';
+}
+
+
+sub report {
+ my ($bit, $path, $text) = @_;
+
+ my $f = load_template($bit);
+ $f =~ s/((\$[\w:]+)|(\$\{[\w:]+\}))/$1 . "||''"/gee;
+ return $f;
+}
+
+sub start {
+ if ($blosxom::static_or_dynamic eq 'dynamic') {
+ debug(1, "start() called, enabled");
+ while (<DATA>) {
+ last if /^(__END__)?$/;
+ my ($flavour, $comp, $txt) = split ' ',$_,3;
+ $txt =~ s:\\n:\n:g;
+ $template{$flavour}{$comp} = $txt;
+ }
+ return 1;
+ } else {
+ debug(1, "start() called, but in static mode -- not enabling");
+ return 0;
+ }
+}
+
+sub filter {
+ my ($pkg, $files_ref) = @_;
+ my $datepart;
+ my $path_info = path_info();
+
+ debug(2, "filter() called, path_info = $path_info");
+
+ # handle normal cases as fast as possible -- no path, a category
+ # path, or a category + file that exists
+ return 1 if ($path_info eq '');
+ return 1 if ($path_info !~ s!\.[^.]+$!.$blosxom::file_extension!);
+ return 1 if (defined($files_ref->{"$blosxom::datadir$path_info"}));
+
+ debug(2, "fasttrack failed, splitting path");
+
+ # at this point, $path_info is (category + optional date + filename)
+ # and either the file doesn't exist in that category, or the
+ # date field is being used along with a full filename
+
+ # this is straight from blosxom itself, and should be kept in-sync
+ my @path_info = split '/', $path_info;
+ my $filename = pop @path_info;
+ return 1 if ($filename eq "index.$blosxom::file_extension");
+ shift @path_info; # remove empty '' before first /
+ $path_info = '';
+ while ($path_info[0] and
+ $path_info[0] =~ /^[a-zA-Z].*$/ and
+ $path_info[0] !~ /(.*)\.(.*)/) {
+ $path_info .= '/' . shift @path_info;
+ }
+
+ debug(2, "path_info=$path_info, filename=$filename");
+
+ # @path_info may have date info in it still, but we're not interested
+
+ return 1 if defined($files_ref->{"blosxom::datadir$path_info/$filename"});
+
+ debug(2, "Still not found, looking for good matches");
+
+ # okay, it doesn't exist -- it's okay to spend some time now, since
+ # slow results are better than no results
+
+ # XXX this should be quite a bit smarter -- 'sounds like', 'typoed like'
+ # look at what mod_speling does
+ $activated = 1;
+ foreach (keys %$files_ref) {
+ my ($this_filename) = m:([^/]+)$:;
+ if ($filename eq $this_filename) {
+ push(@goodhits, $_);
+ debug(2, "Found good hit: $_");
+ }
+ }
+ $files_ref->{"$blosxom::datadir$path_info/$filename"} =
+ $#goodhits == 0 ? $files_ref->{$goodhits[0]} : time;
+ return 1;
+}
+
+sub story {
+ return 1 if !$activated;
+ my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
+
+ if ($#goodhits == -1) {
+ debug(2, "in story(), no good hits");
+ $$title_ref = report('not_found_title');
+ $$body_ref = report('not_found_body');
+ } elsif ($#goodhits == 0) {
+ debug(2, "in story(), 1 good hit");
+ # just one, easy case to deal with
+ my $fh = new FileHandle;
+ if ($fh->open($goodhits[0])) {
+ debug(3, "opened $goodhits[0]");
+ # convert from filename to path+filename (w/o extension)
+ $goodhits[0] =~ s!$blosxom::datadir(.*)\.[^./]+$!\1!;
+ chomp(my $title = <$fh>);
+ $$title_ref = report('found_one_title', $goodhits[0], $title);
+ $$body_ref = report('found_one_body',$goodhits[0],(join '',<$fh>));
+ } else {
+ debug(3, "Couldn't open $goodhits[0]: $!");
+ $goodhits[0] =~ s!$blosxom::datadir(.*)\.[^./]+$!\1!;
+ $$title_ref = report('error_title', $goodhits[0]);
+ $$body_ref = report('error_body', $goodhits[0]);
+ }
+ } else {
+ debug(2, "in story(), multiple matches");
+ $$title_ref = report('multi_title');
+ $$body_ref = report('multi_body_head');
+ map {
+ $_ =~ s!$blosxom::datadir(.*)\.[^./]+$!\1!;
+ $$body_ref .= report('multi_body_item', $_)
+ } @goodhits;
+ $$body_ref .= report('multi_body_foot');
+ }
+ return 1;
+}
+
+1;
+__DATA__
+html not_found_title Not Found
+html not_found_body <p>The file you asked for doesn't exist, and I'm afraid I couldn't find a good match for it. I'm sorry.</p>\n
+html found_one_title $text
+html found_one_body <p>The file you asked for doesn't exist; it may have been moved. This is actually <a href="$blosxom::url$path.$blosxom::flavour">$path</a>.</p><hr>$text
+html error_title Error
+html error_body <p>The file you asked for doesn't exist, and I thought I'd found a replacement with $path, but I can't open it. Sorry</p>
+html multi_title Possible Matches
+html multi_body_head <p>The file you asked for doesn't exist. Some possible matches are:</p><ul>\n
+html multi_body_item <li><a href="$blosxom::url$path.$blosxom::flavour">$path</a></li>\n
+html multi_body_foot </ul>\n
+__END__
--- /dev/null
+# Blosxom Plugin: autotrack -*- cperl -*-
+# Author: Todd Larason (jtl@molehill.org)
+# Version: 0+2i
+# Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
+# AutoTrack plugin Home/Docs/Licensing:
+# http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/AutoTrack/
+package autotrack;
+
+# -------------- Configuration Variables --------------
+
+# regular expression matching URLs not to trackback
+# there's no reason to try google or amazon, and most people don't
+# want to trackback their own stories
+$dont_tb_re = qr!(?:http://(?:
+ (?: www\.google\.com ) |
+ (?: www\.amazon\.com )
+ )
+ ) |
+ (?: $blosxom::url )!ix
+ unless defined $dont_tb_re;
+
+# what to do if the timestamp file doesn't exist? if $start_from_now is
+# set, autotrack future stories but not old ones
+$start_from_now = 1 unless defined $start_from_now;
+
+# how automatic? if $semi_auto is set, only send trackbacks if ?autotrack=yes
+# otheriwse, fully automatic
+$semi_auto = 1 unless defined $semi_auto;
+
+# networking implementation to be used
+# can be 'LWP', 'curl' or 'wget' (or a full pathname to a curl or wget
+# executable)
+# wget must be at least a 1.9 beta to support the --post-data option
+$networking = "LWP" unless defined $networking;
+
+$debug_level = 1
+ unless defined $debug_level;
+
+# -----------------------------------------------------
+\f
+# template-visible vars
+\f
+use CGI qw/param/;
+use HTML::Entities;
+use File::stat;
+use strict;
+use vars qw/$dont_tb_re $start_from_now $semi_auto $networking $debug_level/;
+\f
+my $dont_really_ping = 0;
+my $package = "autotrack";
+my $timestamp_file = "$blosxom::plugin_state_dir/.$package.timestamp";
+my $last_timestamp;
+my $files;
+\f
+sub debug {
+ my ($level, @msg) = @_;
+
+ if ($debug_level >= $level) {
+ print STDERR "$package debug $level: @msg\n";
+ }
+}
+\f
+# utility funcs
+sub url_escape {
+ local ($_) = @_;
+
+ s/([^a-zA-Z0-9])/sprintf("%%%02x",ord($1))/eg;
+ s/%20/+/g;
+ return $_;
+}
+\f
+sub GET {
+ my ($url) = @_;
+
+ if ($networking =~ m:curl:) {
+ return `$networking -m 30 -s $url`;
+ } elsif ($networking =~ m:wget:) {
+ return `$networking --quiet -O - $url`;
+ } elsif ($networking eq 'LWP') {
+ foreach (qw/LWP::UserAgent HTTP::Request::Common/) {
+ eval "require $_";
+ if ($@) {
+ debug(0, "Can't load $_, can't use LWP networking");
+ return undef;
+ }
+ }
+ my $ua = LWP::UserAgent->new;
+ my $res = $ua->request(HTTP::Request::Common::GET $url);
+ if (!$res->is_success) {
+ my $error = $res->status_line;
+ debug(0, "HTTP GET error: $error");
+ return undef;
+ }
+ return $res->content;
+ } else {
+ debug(0, "ERROR: invalid \$networking $networking");
+ }
+}
+
+sub POST {
+ my ($url, %vars) = @_;
+
+ if ($networking =~ m:curl:) {
+ my $command = "$networking -m 30 -s ";
+ $command .= join ' ',
+ map {my $v = url_escape($vars{$_}); "-d $_=$v";} keys %vars;
+ $command .= " $url";
+ debug(2, "Posting with :$command:");
+ return `$command`
+ unless $dont_really_ping;
+ return "<error>0</error>"; # for testing
+ } elsif ($networking =~ m:wget:) {
+ my $command = "$networking --quiet -O - --post-data='";
+ $command .= join '&',
+ map {my $v = url_escape($vars{$_}); "$_=$v"} keys %vars;
+ $command .= "' $url";
+ debug(2, "Posting with :$command:");
+ return `$command`
+ unless $dont_really_ping;
+ return "<error>0</error>"; # for testing
+ } elsif ($networking eq 'LWP') {
+ foreach (qw/LWP::UserAgent HTTP::Request::Common/) {
+ eval "require $_";
+ if ($@) {
+ debug(0, "Can't load $_, can't use LWP networking");
+ return undef;
+ }
+ }
+ my $ua = LWP::UserAgent->new;
+ my $res = $ua->request(HTTP::Request::Common::POST($url, [%vars]));
+ if (!$res->is_success) {
+ my $error = $res->status_line;
+ debug(0, "HTTP POST error: $error");
+ return undef;
+ }
+ return $res->content;
+ } else {
+ debug(0, "ERROR: invalid \$networking $networking");
+ }
+}
+\f
+sub get_trackback_url {
+ my ($url) = @_;
+
+ return undef if ($url =~ m:$dont_tb_re:);
+ my $text = GET($url);
+ return undef if (!defined($text));
+
+ while ($text =~ m!(<rdf:RDF.*?</rdf:RDF>)!msg) {
+ my $rdf = $1;
+ my ($id) = ($rdf =~ m!dc:identifier="([^\"]+)"!);
+ next unless ($id eq $url);
+ my ($tb) = ($rdf =~ m!trackback:ping="([^\"]+)"!);
+ return $tb if defined $tb;
+ }
+ if ($url =~ m:(.*)\#:) {
+ $url = $1;
+ while ($text =~ m!(<rdf:RDF.*?</rdf:RDF>)!msg) {
+ my $rdf = $1;
+ # XXX is this good enough? Can't the namespace IDs be different?
+ # the sample code in the spec @
+ # http://www.movabletype.org/docs/mttrackback.html
+ # does it this way
+ my ($id) = ($rdf =~ m!dc:identifier="([^\"]+)"!);
+ next unless ($id eq $url);
+ my ($tb) = ($rdf =~ m!trackback:ping="([^\"]+)"!);
+ return $tb if defined $tb;
+ }
+ }
+ debug(2, "Couldn't find tb url for $url");
+ return undef;
+}
+
+sub ping_trackback {
+ my ($tb_url, $title, $excerpt, $url) = @_;
+
+ my $txt = POST($tb_url, title => $title, url => $url, excerpt => $excerpt,
+ blog_name => $blosxom::blog_title);
+ debug(3, "Response:$txt:");
+ if ($txt =~ m:<error>(.*?)</error>:ms) {
+ if ($1) {
+ my $errcode = $1;
+ $txt =~ m:<message>(.*)</message>:ms;
+ debug(0, "Error $errcode ($1) pinging $tb_url");
+ return 0;
+ }
+ return 1;
+ }
+ debug(0, "Malformed response while pinging $tb_url");
+ return 1;
+}
+
+sub make_excerpt {
+ my ($story) = @_;
+
+ # XXX options to use plaintext or foreshortened plugins
+
+ $story =~ s:<.+?>::msg;
+ $story = substr($story, 0, 255);
+
+ return $story;
+}
+\f
+# plugin funcs
+sub start {
+ return 0 unless $networking;
+ return 0 if ($semi_auto && !param('autotrack'));
+ # XXX there are at least two different race conditions here
+ # 1: two instances running at the same time could both see an old
+ # timestamp
+ # 2: an instance run at the same time a new story is published could
+ # (at least if there's any clock skew at all) create a timestamp
+ # file >= timestamp(story), for a story that isn't seen.
+ $last_timestamp = -e $timestamp_file ? stat($timestamp_file)->mtime :
+ ($start_from_now ? time : 0);
+ my $fh = new FileHandle;
+ if (!$fh->open("> $timestamp_file")) {
+ debug(0, "Couldn't touch timestamp file $timestamp_file");
+ return 0;
+ }
+ $fh->close;
+
+ debug(1, "autotrack enabled");
+
+ return 1;
+}
+
+sub filter {
+ my ($pkg, $files_ref) = @_;
+
+ $files = $files_ref;
+
+ return 1;
+}
+
+sub story {
+ my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
+ my ($pathname) = "$blosxom::datadir$path/$filename.$blosxom::file_extension";
+ return 1 if ($files->{$pathname} < $last_timestamp);
+ my (%pinged, $ping_tries, $ping_succeed);
+ my $excerpt = make_excerpt($$body_ref);
+ my $url = "$blosxom::url$path/$filename.writeback";
+ defined $meta::tb_ping and
+ ++$ping_tries and
+ ping_trackback($meta::tb_ping, $$title_ref, $excerpt, $url) and
+ ++$ping_succeed and
+ ++$pinged{$meta::tb_ping};
+ return 1 if (defined $meta::autotrack && $meta::autotrack eq 'no');
+ while ($$body_ref =~
+ m!<a\s [^>]*
+ href=(?:
+ (http://[^ ]+) |
+ "(http://[^\"]+)"
+ )!msxg) {
+ my $trackback_url = get_trackback_url(decode_entities($+));
+ next unless defined $trackback_url;
+ next if $pinged{$trackback_url};
+ $ping_tries++;
+ ping_trackback($trackback_url, $$title_ref, $excerpt, $url) and
+ ++$ping_succeed and
+ ++$pinged{trackback_url};
+ debug(1, "autotracked: $trackback_url");
+ }
+
+ # XXX what do we do if some succeed and some fail?
+ # If we tried some but they all failed, revert the timestamp to
+ # try again later
+ if ($ping_succeed == 0 && $ping_tries > 0) {
+ debug(0, "All pings failed, reverting timestamp");
+ utime($last_timestamp, $last_timestamp, $timestamp_file);
+ }
+
+ 1;
+}
+1;
+=head1 NAME
+
+ Blosxom Plug-in: autotrack
+
+=head1 SYNOPSIS
+
+ Automatically or semi-automatically sends trackback pings for new stories.
+=head1 VERSION
+
+ 0+2i
+
+ 2nd test release
+=head1 AUTHOR
+
+ Todd Larason <jtl@molehill.org> http://molelog.molehill.org/
+
+=head1 BUGS
+
+ None known; address bug reports and comments to me or to the Blosxom
+ mailing list [http://www.yahoogroups.com/groups.blosxom].
+
+=head1 Trackback Ping URL Discovery
+
+ Trackback Ping URLs are discovered two different ways.
+
+=head2 Manual Ping URLs
+
+ If you have the meta plugin installed, and have it set to run prior to the
+ autotrack plugin, you can give a trackback url with the "meta-tb_ping"
+ header; the value of the header should be the ping URL to ping.
+
+=head2 Automatic Ping URL detection
+
+ Subject to some exceptions explained below, every URL given in an 'href' in
+ the story is fetched, and the resulting content is searched for embedded RDF
+ sections giving trackback URLs for the given URL. This is the preferred way
+ for all tools to be given trackback URLs, as it requires no human
+ intervention, but unfortunately not everyone which has a trackback server
+ includes the appropriate RDF. Even more unfortunately, there's no easy
+ way to know whether it's included or not, other than examining the source
+ of the page.
+
+ It's always safe to give a meta-tb_ping header; if you give one, and the
+ same ping URL is found by autodiscovery, it's only pinged once.
+
+ If you don't want autodiscovery to be used for a given story, you can set
+ the meta header 'meta-autotrack' to 'no'. If "meta-autotrack: no" is given,
+ the meta-tb_ping URL is still pinged if it's specified.
+
+=head1 Customization
+
+=head2 Configuration Variables
+
+ C<$dont_tb_re> is a regular expression agains which URLs are matched;
+ if it matches, the URL isn't fetched for autodiscovery; this is useful
+ for classes of URLs that you link to frequently that you know don't
+ include the autodiscovery RDF, or that you don't wish to be pinged. The
+ default value matches Amazon and Google URLs, as well as references to
+ the current weblog.
+
+ C<$start_from_now> is a boolean that controls the behavior if the timestamp
+ file doesn't exist; if it's true, then it's treated as if it does exist,
+ with the current time -- no old articles are pinged. If it's false, then
+ every story seen is treated as new. Defaults to true.
+
+ C<$semi_auto> is a boolean controlling how automatic the pinging is. If
+ it's false, then the plugin acts in fully automatic mode -- it's always
+ enabled, and any new story is examined. If it's true, then the plugin
+ acts in semi-automatic mode -- it's only enabled if the URL being browsed
+ includes the paramater "autotrack" (ie, ends with "?autotrack=yes"). By
+ default, this is true.
+
+ C<$networking> controls which networking implementation to use. If set to
+ "LWP", an implementation which uses the common LWP (libwww-for-perl) perl
+ module set is used; if set to a string that includes the word 'curl', an
+ implementation which uses the external 'curl' utility is used, and the value
+ of $networking is used as the beginning of the command line (this can be used
+ to specify a full path to curl or to pass additional arguments); if set
+ to a string which includes the word 'wget', an implementation which uses the
+ external 'wget' utility is used with $networking used at the beginning of
+ the command line as with curl. The wget executable must be new enough to
+ include the --post-data option; currently, that means a recent 1.9 beta.
+ Defaults to "LWP".
+
+ C<$debug_level> is an int from 0 to 5 controlling how much debugging output
+ is logged; 0 logs only errors. Defaults to 1.
+
+=head2 CSS and Flavour Files
+
+ There is no output, so no customization through these methods.
+
+=head1 Timestamp
+
+ A timestamp file is kept as $plugin_state_dir/.autotrack.timestamp; stories
+ are considered 'new' if their timestamp is later than the timestamp file
+ (see the C<$start_from_now> variable for the behavior if the file doesn't
+ exist). There is a small race condition between reading the timestamp
+ file and updating it when the plugin is enabled; one advantage of semi-
+ automatic mode is that this is rarely a problem, since the plugin is only
+ enabled when you want it to be.
+
+ If trackback pings are attempted but they all fail, the timestamp file is
+ reverted to its previous value, so the pings will be tried again later. if
+ some pings succeed and others fail, however, the timestamp is left with the
+ updated values, and the failed pings won't be retried.
+
+=head1 THANKS
+
+ * Rael Dornfest -- blosxom (of course) and suggesting $start_from_now option
+ * Taper Wickel -- pointing out wget 1.9's post support
+
+=head1 LICENSE
+
+this Blosxom Plug-in
+Copyright 2003, Todd Larason
+
+(This license is the same as Blosxom's)
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+=cut
+
--- /dev/null
+# Blosxom Plugin: blogroll -*- perl -*-
+# Author: Todd Larason (jtl@molehill.org)
+# Author: Kevin Scaldeferri (kevin@scaldeferri.com)
+# (line and version added by Doug Nerad to show latest version)
+# Version: 0+5i
+# Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
+# Blogroll plugin Home/Docs/Licensing:
+# http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/Blogroll/
+
+package blogroll;
+
+# -------------- Configuration Variables --- -------------
+
+# files to read; should be either OPML, NewNewsWire .plist, or 'table'
+# files (<title>tab<url>\n); with the default, just create a directory
+# "$plugin_state_dir/.blogroll" and put your files (or symlinks to
+# them) in it
+
+@source_files = glob "$blosxom::plugin_state_dir/.blogroll/*" if ($#source_files < 0);
+
+$use_caching = 1 unless defined $use_caching;
+
+$debug_level = 0 unless defined $debug_level;
+# -------------------------------------------------------------------
+
+use IO::File;
+use File::stat;
+
+my $package = 'blogroll';
+my $cachefile = "$blosxom::plugin_state_dir/.$package.cache";
+my $save_cache = 0;
+my $cache;
+
+sub debug {
+ my ($level, @msg) = @_;
+
+ $debug .= "@msg<br>\n";
+ if ($debug_level >= $level) {
+ print STDERR "$package debug $level: @msg\n";
+ }
+}
+
+sub load_template {
+ my ($bit) = @_;
+ return $blosxom::template->('', "$package.$bit", $blosxom::flavour);
+}
+\f
+# Output & formatting functions
+
+sub report {
+ my ($bit, $title, $htmlurl, $xmlurl) = @_;
+
+ my $f = load_template($bit);
+ $f =~ s/((\$[\w:]+)|(\$\{[\w:]+\}))/$1 . "||''"/gee;
+ return $f;
+}
+
+sub finish_file_tree {
+ my ($tree) = @_;
+ my $results;
+
+ if ($tree->{items}) {
+ $results .= report('sub_head', $tree->{title});
+ $results .= finish_file_tree($_) foreach @{$tree->{items}};
+ $results .= report('sub_foot', $tree->{title});
+ } else {
+ $results .= report($tree->{xmlurl} ? 'item_xml':'item_no_xml',
+ $tree->{title}, $tree->{htmlurl}, $tree->{xmlurl});
+ }
+ return $results;
+}
+
+sub finish_file {
+ my ($fc, $filename, $tree) = @_;
+ local $_;
+ my $results;
+
+ $filename =~ s:.*/::;
+ $filename =~ s:[^a-zA-Z0-9]+:_:g;
+ $$filename = $fc->{blogroll}{$blosxom::flavour};
+ return if defined $$filename;
+
+ $results = report('head');
+ foreach (@{$tree->{items}}) {
+ $results .= finish_file_tree($_);
+ }
+ $results .= report('foot');
+
+ $$filename = $fc->{blogroll}{$blosxom::flavour} = $results;
+}
+
+sub finish {
+ my (@filenames) = @_;
+ my $key = '';
+
+ foreach (@filenames) {
+ my $fc = $cache->{file}{$_};
+ $key .= "|$fc->{mtime}";
+ finish_file($fc, $_, $fc->{tree}) if ($fc->{tree});
+ }
+ return $cache->{blogroll}{$blosxom::flavour}
+ if ($cache->{blogroll_key}{$blosxom::flavour} eq $key);
+
+ debug(1, "cache miss: blogroll results: $key");
+ my @items;
+ foreach my $filename (@filenames) {
+ my $fc = $cache->{file}{$filename};
+ foreach (@{$fc->{items}}) {
+ push @items, $_;
+ }
+ }
+
+ my $results;
+
+ $results = report('head');
+ foreach (sort {lc($a->[0]) cmp lc($b->[0])} @items) {
+ $results .= report(defined($_->[2]) ? 'item_xml':'item_no_xml', @{$_});
+ }
+ $results .= report('foot');
+
+ $cache->{blogroll_key}{$blosxom::flavour} = $key;
+ $cache->{blogroll}{$blosxom::flavour} = $results;
+ $save_cache = 1;
+
+ return $results;
+}
+\f
+# input and parsing functions
+
+sub handle_item {
+ my ($fc, @record) = @_;
+ push @{$fc->{items}}, [@record];
+ debug(3, "handle_item(@record)");
+}
+
+sub handle_tree {
+ my ($fc, $tree) = @_;
+
+ if ($tree->{items}) {
+ handle_tree($fc, $_) foreach @{$tree->{items}};
+ } else {
+ handle_item($fc, $tree->{title}, $tree->{htmlurl}, $tree->{xmlurl});
+ }
+}
+
+sub handle_opml_subscription_file {
+ my ($fh, $fc) = @_;
+ my $count = 0;
+ # XXX this should maybe do 'real' xml parsing
+ # XML::Simple fast enough? worth requiring more
+ # modules installed?
+ my $text = join '',<$fh>;
+ while ($text =~ m!\s<outline (.*?)>!msg) {
+ $_ = $1;
+ next unless m|/$|;
+ my ($htmlurl, $title, $xmlurl);
+ ($htmlurl) = m:html[uU]rl=" ( [^\"]+ ) ":x;
+ ($title ) = m:title =" ( [^\"]+ ) ":x;
+ ($xmlurl ) = m:xml[uU]rl =" ( [^\"]+ ) ":x;
+ if (defined($title) && (defined($htmlurl) || defined($xmlurl))) {
+ push @{$fc->{tree}{items}},
+ {title => $title,
+ htmlurl => $htmlurl,
+ xmlurl => $xmlurl};
+ $count++;
+ }
+ }
+ debug(2, "handle_opml_subscription_file finished, $count items");
+}
+
+sub handle_tab_file {
+ my ($fh, $fc) = @_;
+ my $count = 0;
+ while ($_ = $fh->getline) {
+ chomp;
+ my ($title, $htmlurl) = split /\t+/;
+ push @{$fc->{tree}{items}},
+ {title => $title,
+ htmlurl => $htmlurl,
+ xmlurl => $xmlurl};
+ }
+ debug(2, "handle_tab_file finished, $count items");
+}
+
+sub read_plist_dict {
+ my ($fh) = @_;
+ my $self = { type => 'dict'};
+
+ my ($key, $value);
+ while ($_ = $fh->getline) {
+ if (m!<key>(.*)</key>!) {
+ $key = $1;
+ } elsif (m!<array>!) {
+ $self->{$key} = read_plist_array($fh);
+ } elsif (m!<array/>!) {
+ $self->{$key} = {type => 'array', array => []};
+ } elsif (m!<string>(.*)</string>!) {
+ $self->{$key} = $1;
+ } elsif (m!</dict>!) {
+ return $self;
+ } else {
+ die "$_ in dict";
+ }
+ }
+}
+
+sub read_plist_array {
+ my ($fh) = @_;
+ my $self = { type => 'array'};
+
+ $self->{array} = [];
+ while ($_ = $fh->getline) {
+ if (/<dict>/) {
+ push @{$self->{array}}, read_plist_dict($fh);
+ } elsif (m!</array>!) {
+ return $self;
+ } else {
+ die "$_ in <array>";
+ }
+ }
+}
+
+sub prettify_plist_tree {
+ my ($tree) = @_;
+
+ if ($tree->{type} eq 'array') {
+ return [map {prettify_plist_tree($_)} @{$tree->{array}}];
+ } elsif ($tree->{isContainer}) {
+ return {title => $tree->{name},
+ items => prettify_plist_tree($tree->{childrenArray})};
+ } elsif ($tree->{type} eq 'dict' &&
+ $tree->{name} && $tree->{home} && $tree->{rss}) {
+ return {title => $tree->{name},
+ htmlurl => $tree->{home},
+ xmlurl => $tree->{rss}};
+ } else {
+ die "Unexpected node: $tree->{type}";
+ }
+}
+
+sub handle_nnw_file {
+ my ($fh, $fc) = @_;
+ my $count = 0;
+
+ do {
+ $_ = $fh->getline
+ } while ($_ && !m!<key>Subscriptions</key>!);
+ $_ = $fh->getline;
+ m:<array>: or die "Unexpected format: $_ at nnw toplevel";
+ my $tree = read_plist_array($fh);
+ $fc->{tree} = {items => prettify_plist_tree($tree)};
+}
+
+sub handle_file {
+ my ($filename) = @_;
+
+ my $filecache = $cache->{file}{$filename};
+ my $mtime = stat($filename)->mtime;
+
+ # If this file is in the cache, and hasn't been modified, we're
+ # done here
+ return if (defined($filecache) && $filecache->{mtime} == $mtime);
+
+ debug(1, "cache miss $filename: $mtime");
+
+ # Either not there or outdated, start over
+ $filecache = {mtime => $mtime, items => []};
+
+ my $fh = new IO::File("< $filename");
+ if (!$fh) {
+ warn "Couldn't open $filename";
+ return;
+ }
+
+ if ($filename =~ m:\.opml$:) {
+ handle_opml_subscription_file($fh, $filecache)
+ } elsif ($filename =~ m:\.tab$:) {
+ handle_tab_file($fh, $filecache);
+ } elsif ($filename =~ m:/com\.ranchero\.NetNewsWire\.plist:) {
+ handle_nnw_file($fh, $filecache);
+ } else {
+ warn "Unrecognized filetype $filename";
+ }
+ $fh->close;
+ handle_tree($filecache, $filecache->{tree});
+
+ $cache->{file}{$filename} = $filecache;
+}
+\f
+# blosxom plugin interface
+
+$blogroll;
+$last_flavour = '';
+
+sub prime_cache {
+ return 0 if !$use_caching;
+ eval "require Storable";
+ if ($@) {
+ debug(1, "cache disabled, Storable not available");
+ $use_caching = 0;
+ return 0;
+ }
+ if (!Storable->can('lock_retrieve')) {
+ debug(1, "cache disabled, Storable::lock_retrieve not available");
+ $use_caching = 0;
+ return 0;
+ }
+ $cache = (-r $cachefile ? Storable::lock_retrieve($cachefile) : undef);
+ # for this, the cache is always valid if it exists
+ if (defined($cache)) {
+ debug(1, "Loaded cache");
+ return 1;
+ }
+ $cache = {};
+ return 0;
+}
+
+sub save_cache {
+ return if (!$use_caching || !$save_cache);
+ debug(1, "Saving cache");
+ Storable::lock_store($cache, $cachefile);
+}
+
+sub start {
+ debug(1, "start() called, enabled");
+ while (<DATA>) {
+ chomp;
+ last if /^(__END__)?$/;
+ my ($flavour, $comp, $txt) = split ' ',$_,3;
+ $txt =~ s:\\n:\n:g;
+ $blosxom::template{$flavour}{"$package.$comp"} = $txt;
+ }
+ prime_cache();
+ return 1;
+}
+
+sub head {
+ my ($pkg, $currentdir, $head_ref) = @_;
+
+ local $_;
+
+ # for static generation, don't do the same work over and over
+
+ return 1 if ($blogroll && $last_flavour eq $blosxom::flavour);
+ $last_flavour = $blosxom::flavour;
+
+ debug(1, "head() called");
+ foreach my $filename (@source_files) {
+ handle_file($filename) ;
+ }
+ $blogroll = finish(@source_files);
+ debug(1, "head() finished, length(\$blogroll) =", length($blogroll));
+
+ save_cache();
+ 1;
+}
+
+1;
+# default flavour files; the 'error' flavour is default
+# 'blogroll.' is prepended to the name given here
+# to create an html flavour, then, create files 'blogroll.head.html' and so on.
+__DATA__
+error head <ul class="blogroll">\n
+error sub_head <li>$title<ul>\n
+error item_no_xml <li><a href="$htmlurl">$title</a></li>\n
+error item_xml <li><a href="$htmlurl">$title</a> (<a href="$xmlurl">xml</a>)</li>\n
+error sub_foot </ul></li>\n
+error foot </ul>\n
+__END__
+
+=head1 NAME
+
+Blosxom Plug-in: blogroll
+
+=head1 SYNOPSIS
+
+Purpose: Provides a blogroll from pre-exsting data files and/or an simple text file
+
+ * $blogroll::blogroll -- blogroll, sorted, combined from all input files
+ * $blogroll::<sanitized filename> -- blogroll of items from C<filename>,
+ in their original order. <sanitized filename> is C<filename> with all
+ non-alphanumerics replaced with underscores
+
+=head1 VERSION
+
+0+4i
+
+4th test release
+
+=head1 AUTHOR
+
+Todd Larason <jtl@molehill.org>, http://molelog.molehill.org/
+
+=head1 BUGS
+
+None known; address bug reports and comments to me or to the Blosxom
+mailing list [http://www.yahoogroups.com/groups.blosxom].
+
+=head1 Customization
+
+=head2 Input files
+
+Three file formats are currently supported
+
+=head3 OPML subscription files
+
+These are recognized by a '.opml' extension.
+
+Only subscription files are supported; general OPML files are not. Although
+OPML itself is standardized, the subscription subset is not, and there's
+more variation than you might expect. This is known to work with AmphetaDesk
+and Radio native subscription files (but not Radio's other OPML files), and
+NetNewsWire export files; I'm interested in both success and failure reports
+for files from other OPML generators.
+
+=head3 TAB files
+
+These are recognized by a '.tab' extension.
+
+This is a simple text format intended for human editing, either to supplment
+the items from the other file formats or for people who don't wish to use
+one of the others.
+
+Each line represents a record. Each record contains two fields, separated
+by a tab. The first field is the name of the item, the second feld is the
+URL.
+
+=head3 NNW plist files
+
+These are recognized by the full name "com.ranchero.NetNewsWire.plist" (there
+may be other plist formats supported in the future, so ".plist" isn't enough).
+
+This is the native subscription format for NetNewsWire and NetNewsWire Pro.
+
+This format supports hierarchical categorization of entries, available via the
+$blogroll::com_ranchero_NetNewsWire_plist variable.
+
+=head2 Configuration variables
+
+C<@source_files> is the list of files to be used; by default, it's all the
+files in $blosxom::plugin_state_dir/.blogroll.
+
+C<$use_caching> controls whether or not to try to cache data and
+formatted results; caching requires Storable, but the plugin will work
+just fine without it.
+
+C<$debug_level> can be set to a value between 0 and 5; 0 will output
+no debug information, while 5 will be very verbose. The default is 1,
+and should be changed after you've verified the plugin is working
+correctly.
+
+=head2 Class for CSS control
+
+There's a class used, available for CSS customization.
+
+ * C<blogroll> -- the blogroll list as a whole
+
+=head2 Flavour-style files
+
+If you want a format change that can't be made by CSS, you can
+override the HTML generated by creating files similar to Blosxom's
+flavour files. They should be named blogroll.I<bit>.I<flavour>; for
+available I<bit>s and their default meanings, see the C<__DATA__>
+section in the plugin.
+
+=head1 Caching
+
+If the Storable module is available and $use_caching is set, various
+bits of data will be cached; this includes the parsed items from the
+input files and the final formatted output of any blogrolls generated.
+
+The cache will never be entirely flushed, but relevant pieces are invalidated
+when input files are modified. If you're making template changes,
+you may wish to either disable the cache (by setting $use_caching to 0) or
+manually flush the cache; this can be done by removing
+$plugin_state_dir/.calendar.cache, and is always safe to do.
+
+=head1 LICENSE
+
+this Blosxom Plug-in
+Copyright 2003, Todd Larason
+
+(This license is the same as Blosxom's)
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+
--- /dev/null
+# Blosxom Plugin: calendar -*- perl -*-
+# Author: Todd Larason (jtl@molehill.org)
+# Version: 0+6i
+# Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
+# Calendar plugin Home/Docs/Licensing:
+# http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/Calendar/
+
+package calendar;
+
+# --- Configuration Variables ---
+
+@monthname = qw/January February March
+ April May June
+ July August September
+ October November December/ if ($#monthname != 11);
+@monthabbr = qw/Jan Feb Mar
+ Apr May Jun
+ Jul Aug Sep
+ Oct Nov Dec/ if ($#monthabbr != 11);
+@downame = qw/Sunday Monday Tuesday Wednesday Thursday
+ Friday Saturday/ if ($#downame != 6);
+@dowabbr = qw/Sun Mon Tue Wed Thu Fri Sat/ if ($#dowabbr != 6);
+
+$first_dow = 0
+ if not defined $first_dow;
+
+# set to 0 to disable attempted caching
+$use_caching = 1 unless defined $use_caching;
+$months_per_row = 3 unless defined $months_per_row;
+$debug_level = 1 unless defined $debug_level;
+# -------------------------------------------------------------------
+
+use Time::Local;
+
+$month_calendar = '';
+$year_calendar = '';
+$calendar = '';
+$prev_month_link = '';
+$next_month_link = '';
+$prev_year_link = '';
+$next_year_link = '';
+
+my $package = "calendar";
+my $cachefile = "$blosxom::plugin_state_dir/.$package.cache";
+my $cache;
+my $save_cache = 0;
+my $files;
+\f
+sub debug {
+ my ($level, @msg) = @_;
+
+ if ($debug_level >= $level) {
+ print STDERR "$package debug $level: @msg\n";
+ }
+ 1;
+}
+
+sub load_template {
+ my ($bit) = @_;
+ return $blosxom::template->('', "$package.$bit", $blosxom::flavour);
+}
+
+sub report {
+ my ($bit, $year, $month, $day, $dow) = @_;
+ my ($monthname, $monthabbr) = ($monthname[$month-1], $monthabbr[$month-1]);
+ my ($downame, $dowabbr) = ($downame[$dow], $dowabbr[$dow]);
+ my $year2digit = sprintf("%02d", $year % 100);
+
+ my $url = $blosxom::url;
+ $url .= sprintf("/%04d/", $year) if defined $year;
+ $url .= sprintf("%02d/", $month) if defined $month;
+ $url .= sprintf("%02d/", $day) if defined $day;
+
+ my $date = '';
+ $date .= "$year" if defined $year;
+ $date .= "/$month" if defined $month;
+ $date .= "/$day" if defined $day;
+ my $count = $cache->{stories}{$date};
+
+ my $f = load_template($bit);
+ $f =~ s/((\$[\w:]+)|(\$\{[\w:]+\}))/$1 . "||''"/gee;
+ return $f;
+}
+
+sub days_in_month {
+ my ($year, $month) = @_;
+ my $days = (31,28,31,30,31,30,31,31,30,31,30,31)[$month-1];
+ if ($month == 2 &&
+ ($year % 4 == 0 &&
+ (($year % 100 != 0) ||
+ ($year % 400 == 0)))) {
+ $days++;
+ }
+ return $days;
+}
+
+sub pseudo_now {
+ my ($year, $month, $day);
+
+ if (defined($blosxom::path_info_yr)) {
+ $year = $blosxom::path_info_yr + 0;
+ $month = $blosxom::path_info_mo_num + 0;
+ $day = $blosxom::path_info_da + 0;
+ if ($month == 0 && $cache->{stories}{$year}) {
+ for ($month = 12; $month > 0; $month--) {
+ last if $cache->{stories}{"$year/$month"};
+ }
+ }
+ $month ||= 12;
+ } else {
+ my $now;
+ # is this a single-article view?
+ # XXX this probably doesn't work for static
+ if ($blosxom::path_info =~ m!\.!) {
+ my $filename = $blosxom::path_info;
+ # replace whatever flavour with the file extension
+ $filename =~ s!\..*!.$blosxom::file_extension!;
+ # remove any dated dirs, if present
+ $filename =~ s!/\d+/.*/!/!;
+ # and convert from an URL relative to a filename
+ $filename = "$blosxom::datadir/$filename";
+ # at this point, it's our best guess at a filename
+ # if it's not in the index, oh well, we tried - fall back to present
+ $now = $files->{$filename} || $^T;
+ } else {
+ $now = $^T;
+ }
+ # no date given at all, do this month and highlight today
+ my @now = localtime($now);
+ $year = $now[5] + 1900;
+ $month = $now[4] + 1;
+ $day = $now[3] + 0;
+ }
+ return ($year, $month, $day);
+}
+
+sub build_prev_month_link {
+ my ($year, $month) = @_;
+ my $year_orig = $year;
+ my $month_orig = $month;
+
+ while (1) {
+ $month--;
+ if ($month <= 0) { # == 0 is right, <= 2xprotects against infinite loop bug
+ $year--;
+ $month = 12;
+ # XXX assumption: once a log is active, no full years are skipped
+ if ($cache->{stories}{"$year"} == 0) {
+ return report('prev_month_nolink',
+ $month_orig == 1 ? $year_orig-1 : $year_orig,
+ $month_orig == 1 ? 12 : $month_orig-1);
+ }
+ }
+ if ($cache->{stories}{"$year/$month"}) {
+ return report('prev_month_link', $year, $month);
+ }
+ }
+}
+
+sub build_next_month_link {
+ my ($year, $month) = @_;
+ my $year_orig = $year;
+ my $month_orig = $month;
+
+ while (1) {
+ $month++;
+ if ($month == 13) {
+ $year++;
+ $month = 1;
+ # XXX assumption: once a log is active, no full years are skipped
+ if ($cache->{stories}{"$year"} == 0) {
+ return report('next_month_nolink',
+ $month_orig == 1 ? $year_orig-1 : $year_orig,
+ $month_orig == 1 ? 12 : $month_orig-1);
+ }
+ }
+ if ($cache->{stories}{"$year/$month"}) {
+ return report('next_month_link', $year, $month);
+ }
+ }
+}
+
+sub build_prev_year_link {
+ my ($year) = @_;
+
+ $year--;
+ return report($cache->{stories}{"$year"} ?
+ 'prev_year_link': 'prev_year_nolink',
+ $year);
+}
+
+sub build_next_year_link {
+ my ($year) = @_;
+ my $results;
+
+ $year++;
+ return report($cache->{stories}{"$year"} ?
+ 'next_year_link': 'next_year_nolink',
+ $year);
+}
+
+sub build_month_calendar {
+ my ($year, $month, $highlight_dom) = @_;
+ my $results;
+
+ my (@now, $monthstart, @monthstart);
+ my ($day, $days, $wday);
+ my $future_dom = 0;
+
+ @now = localtime($^T);
+ $future_dom = $now[3]+1 if ($year == $now[5]+1900 && $month == $now[4]+1);
+
+ $days = days_in_month($year, $month);
+ $monthstart = timelocal(0,0,0,1,$month-1,$year-1900);
+ @monthstart = localtime($monthstart);
+
+ $results = report('month_head', $year, $month);
+ $results .= report('month_sub_head', $year, $month);
+ $wday = $first_dow;
+ do {
+ $results .= report('month_sub_day', $year, $month, undef, $wday);
+ $wday++;
+ $wday %= 7;
+ } while ($wday != $first_dow);
+ $results .= report('month_sub_foot', $year, $month);
+
+ # First, skip over the first partial week (possibly empty)
+ # before the month started
+ for ($wday = $first_dow; $wday != $monthstart[6]; $wday++, $wday %= 7) {
+ $results .= report('week_head', $year, $month)
+ if ($wday == $first_dow);
+ $results .= report('noday', $year, $month, undef, $wday);
+ }
+
+ # now do the month itself
+ for ($day = 1; $day <= $days; $day++) {
+ $results .= report('week_head', $year, $month, $day)
+ if ($wday == $first_dow);
+ my $tag;
+ if ($day == $highlight_dom) {
+ if ($cache->{stories}{"$year/$month/$day"}){$tag='this_day_link'}
+ else {$tag = 'this_day_nolink'}}
+ elsif ($cache->{stories}{"$year/$month/$day"}){$tag = 'day_link'}
+ elsif ($future_dom && $day >= $future_dom) {$tag = 'day_future'}
+ else {$tag = 'day_nolink'}
+ $results .= report($tag, $year, $month, $day, $wday);
+ $wday = 0 if (++$wday == 7);
+ $results .= report('week_foot', $year, $month)
+ if ($wday == $first_dow);
+ }
+
+ # and finish up the last week, if any left
+ if ($wday != $first_dow) {
+ for(; $wday != $first_dow; $wday++, $wday %= 7) {
+ $results .= report('noday', $year, $month, undef, $wday);
+ }
+ $results .= report('week_foot', $year, $month);
+ }
+
+ $results .= report('month_foot', $year, $month);
+
+ return $results;
+}
+
+sub build_year_calendar {
+ my ($year, $highlight_month) = @_;
+ my $results;
+ my $month;
+ my $future_month = 0;
+
+ @now = localtime($^T);
+ $future_month = $now[4]+1 if ($year == $now[5]+1900);
+
+ $results = report('year_head', $year);
+ for ($month = 1; $month <= 12; $month++) {
+ $results .= report('quarter_head', $year)
+ if ($month % $months_per_row == 1);
+ my $tag;
+ if ($month == $highlight_month) {
+ if ($cache->{stories}{"$year/$month"}) {$tag = 'this_month_link'}
+ else {$tag = 'this_month_nolink'}}
+ elsif ($cache->{stories}{"$year/$month"}) {$tag = 'month_link'}
+ elsif ($future_month && $month >=$future_month){$tag = 'month_future'}
+ else {$tag = 'month_nolink'}
+ $results .= report($tag, $year, $month);
+ $results .= report('quarter_foot', $year)
+ if ($month % $months_per_row == 0);
+ }
+ $results .= report('year_foot', $year);
+
+ return $results;
+}
+
+sub build_calendar {
+ return report('calendar');
+}
+
+\f
+sub cached {
+ my ($sub, $cachetag, @args) = @_;
+ my $cachekey = join '/',@args,$blosxom::flavour;
+ return $cache->{$cachetag}{$cachekey} ||=
+ (debug(1, "cache miss $cachetag @args $blosxom::flavour") and
+ ($save_cache = 1) and
+ $sub->(@args));
+}
+
+sub prime_cache {
+ my ($num_files) = @_;
+ return 0 if !$use_caching;
+ eval "require Storable";
+ if ($@) {
+ debug(1, "cache disabled, Storable not available");
+ $use_caching = 0;
+ return 0;
+ }
+ if (!Storable->can('lock_retrieve')) {
+ debug(1, "cache disabled, Storable::lock_retrieve not available");
+ $use_caching = 0;
+ return 0;
+ }
+ $cache = (-r $cachefile ? Storable::lock_retrieve($cachefile) : undef);
+ # >= rather than ==, so that if we're being used along with a search
+ # plugin that reduces %files, rather than dumping the cache and showing
+ # a limited calendar, we'll display the full thing (if available) . I
+ # think that's preferable as well as being more efficient.
+ # XXX improvement: rather than dumping the whole thing, just update the
+ # count and dump the current month (and sometimes the previous month
+ # and year)
+ @now = localtime;
+ $now[4] += 1;
+ $now[5] += 1900;
+ $today = "$now[5]/$now[4]/$now[3]";
+ if ($cache &&
+ $cache->{num_files} >= $num_files &&
+ $cache->{today} eq $today) {
+ debug(1, "Using cached state");
+ return 1;
+ }
+ $cache = {num_files => $num_files, today => $today};
+ return 0;
+}
+
+sub save_cache {
+ return if (!$use_caching || !$save_cache);
+ debug(1, "Saving cache");
+ -d $blosxom::plugin_state_dir
+ or mkdir $blosxom::plugin_state_dir;
+ Storable::lock_store($cache, $cachefile);
+}
+\f
+sub start {
+ debug(1, "start() called, enabled");
+
+ while (<DATA>) {
+ chomp;
+ last if /^(__END__)?$/;
+ my ($flavour, $comp, $txt) = split ' ',$_,3;
+ $txt =~ s:\\n:\n:g;
+ $blosxom::template{$flavour}{"$package.$comp"} = $txt;
+ }
+ return 1;
+}
+
+sub filter {
+ my ($pkg, $files_ref) = @_;
+ debug(1, "filter() called");
+
+ $files = $files_ref;
+
+ my $num_files = scalar keys %$files;
+ my @latest = (sort {$b <=> $a} values %$files);
+ while ($latest[0] == $^T) {
+ $num_files--;
+ shift @latest;
+ }
+ return 1 if prime_cache($num_files);
+
+ debug(1, "cache miss: %stories");
+
+ foreach (keys %{$files}) {
+ next if ($_ == $^T);
+ my @date = localtime($files->{$_});
+ my $mday = $date[3];
+ my $month = $date[4] + 1;
+ my $year = $date[5] + 1900;
+ $cache->{stories}{"$year"}++;
+ $cache->{stories}{"$year/$month"}++;
+ $cache->{stories}{"$year/$month/$mday"}++;
+ }
+ debug(1, "filter() done");
+ return 1;
+}
+
+sub head {
+ debug(1, "head() called");
+ my ($year, $month, $day) = pseudo_now();
+ if ($year < 1970) {
+ debug(0,"Bad year $year requested ($year, path_info $ENV{PATH_INFO}, year to 2000");
+ $year = 2000;
+ }
+ $prev_month_link = cached(\&build_prev_month_link, "pml", $year,$month);
+ $next_month_link = cached(\&build_next_month_link, "nml", $year,$month);
+ $prev_year_link = cached(\&build_prev_year_link, "pyl", $year);
+ $next_year_link = cached(\&build_next_year_link, "nyl", $year);
+ $month_calendar = cached(\&build_month_calendar, "mc", $year,$month,$day);
+ $year_calendar = '';
+ for (my $y = (localtime)[5]+1900; $cache->{stories}{$y}; $y--) {
+ my $varname = "year_calendar_$y";
+ if ($y == $year) {
+ $year_calendar = cached(\&build_year_calendar, "yc",$year,$month);
+ $$varname = $year_calendar;
+ } else {
+ $$varname = cached(\&build_year_calendar, "yc", $y);
+ }
+ }
+ $year_calendar = cached(\&build_year_calendar, "yc", $year,$month)
+ unless $year_calendar;
+ $calendar = cached(\&build_calendar, "c", $year,$month,$day);
+
+ save_cache();
+
+ debug(1, "head() done, length(\$month_calendar, \$year_calendar, \$calendar) = ", length($month_calendar), length($year_calendar), length($calendar));
+ return 1;
+}
+
+# these look better (to me), but don't use <caption> like they 'should', and
+# the year one assumes 3 columns
+#html month_head <table class="month-calendar"><tr class="month-calendar-head"><th align="left">$prev_month_link</th><th colspan="5"><a title="$monthname $year" href="$url">$monthname</a></th><th align="right">$next_month_link</th></tr>\n
+#html year_head <table class="year-calendar"><tr class="year-calendar-head"><th align="left">$prev_year_link</th><th><a title="$year" href="$url">$year</a></th><th align="right">$next_year_link</th></tr><tr><th class="year-calendar-subhead" colspan=$months_per_row>Months</th></tr>\n
+
+1;
+__DATA__
+error month_head <table class="month-calendar"><caption class="month-calendar-head">$prev_month_link<a title="$monthname $year ($count)" href="$url">$monthname</a>$next_month_link</caption>\n
+error month_sub_head <tr>\n
+error month_sub_day <th class="month-calendar-day-head $downame">$dowabbr</th>\n
+error month_sub_foot </tr>\n
+error week_head <tr>\n
+error noday <td class="month-calendar-day-noday $downame"> </td>\n
+error day_link <td class="month-calendar-day-link $downame"><a title="$downame, $day $monthname $year ($count)" href="$url">$day</a></td>\n
+error day_nolink <td class="month-calendar-day-nolink $downame">$day</td>\n
+error day_future <td class="month-calendar-day-future $downame">$day</td>\n
+error this_day_link <td class="month-calendar-day-this-day $downame"><a title="$downame, $day $monthname $year (current) ($count)" href="$url">$day</a></td>\n
+error this_day_nolink <td class="month-calendar-day-this-day $downame">$day</td>\n
+error week_foot </tr>\n
+error month_foot </table>\n
+error prev_month_link <a title="$monthname $year ($count)" href="$url">←</a>
+error next_month_link <a title="$monthname $year ($count)" href="$url">→</a>
+error prev_month_nolink ←
+error next_month_nolink →
+error year_head <table class="year-calendar"><caption class="year-calendar-head">$prev_year_link<a title="$year ($count)" href="$url">$year</a>$next_year_link</caption><tr><th class="year-calendar-subhead" colspan="$months_per_row">Months</th></tr>\n
+error quarter_head <tr>\n
+error month_link <td class="year-calendar-month-link"><a title="$monthname $year ($count)" href="$url">$monthabbr</a></td>\n
+error month_nolink <td class="year-calendar-month-nolink">$monthabbr</td>\n
+error month_future <td class="year-calendar-month-future">$monthabbr</td>\n
+error this_month_link <td class="year-calendar-this-month"><a title="$monthname $year ($count)" href="$url">$monthabbr</a></td>
+error this_month_nolink <td class="year-calendar-this-month">$monthabbr</td>
+error quarter_foot </tr>\n
+error year_foot </table>\n
+error prev_year_link <a title="$year ($count)" href="$url">←</a>
+error next_year_link <a title="$year ($count)" href="$url">→</a>
+error prev_year_nolink ←
+error next_year_nolink →
+error calendar <div class="calendar"><table><tr><td>$calendar::month_calendar</td><td>$calendar::year_calendar</td></tr></table></div>
+__END__
+
+=head1 NAME
+
+Blosxom Plug-in: calendar
+
+=head1 SYNOPSIS
+
+Purpose: Provides a Radio-style archive navigation calendar
+
+ * $calendar::calendar -- side-by-side month and year calendars
+ * $calendar::month_calendar -- month calendar only
+ * $calendar::year_calendar -- year calendar only
+ * $calendar::year_calendar_2003 -- year calendar for 2003; these are built for every year with stories.
+
+=head1 VERSION
+
+0+6i
+
+6th test release
+
+=head1 AUTHOR
+
+Todd Larason <jtl@molehill.org>, http://molelog.molehill.org/
+
+=head1 BUGS
+
+None known; address bug reports and comments to me or to the Blosxom
+mailing list [http://www.yahoogroups.com/groups.blosxom].
+
+=head1 Customization
+
+=head2 Configuration variables
+
+C<@monthname>, C<@monthabbr>, C<@downame> and C<@dowabbr> contain the
+long and short forms of the names of the months and days of the week;
+@downame and @dowabbr should always start with Sunday, regardless of
+$first_dow.
+
+C<$first_dow> sets the plugin's idea of the first day day of the week;
+use 0 for Sunday, 1 for Monday, and so on; using a number outside the
+range [0..6] will cause undefined behavior, possibly including a
+nuclear meltdown.
+
+C<$use_caching> controls whether or not to try to cache statistics and
+formatted results; caching requires Storable, but the plugin will work
+just fine without it.
+
+C<$months_per_row> controls how many months are on each row of the
+year calendar. This should be a number that evenly divides 12 (1, 2,
+3, 4, 6 or 12), but nothing checks for that.
+
+C<$debug_level> can be set to a value between 0 and 5; 0 will output
+no debug information, while 5 will be very verbose. The default is 1,
+and should be changed after you've verified the plugin is working
+correctly.
+
+=head2 Classes for CSS control
+
+There's an (over)abundance of classes used, available for CSS customization.
+
+ * C<calendar> -- the calendar as a whole
+ * C<month-calendar> -- the month calendar as a whole
+ * C<month-calendar-head> -- the head of the month calendar (ie,
+ "March")
+ * C<month-calendar-day-head> -- a column head in the month calendar
+ (ie, a day-of-week abbreviation)
+ * C<month-calendar-day-noday>, C<month-calendar-day-link>,
+ C<month-calendar-day-nolink>, C<month-calendar-day-future>,
+ C<month-calendar-day-this-day> -- the day squares on the month
+ calendar, for days that don't exist (before or after the month
+ itself), that don't have stories, that do have stories, that are
+ in the future, or are that currently selected, respectively
+ * Day-of-week-name -- each day square is also given a class matching
+ its day of week, from C<@downame>; this can be used to hilight
+ weekends
+ * C<year-calendar> -- the year calendar as a whole
+ * C<year-calendar-head> -- the head of the year calendar (ie,
+ "2003")
+ * C<year-calendar-subhead> -- ie, "Months"
+ * C<year-calendar-month-link>, C<year-calendar-month-nolink>,
+ C<year-calendar-month-future>, C<year-calendar-this-month> -- the
+ month squares on the year calendar, for months with stories,
+ without, in the future, and currently selected, respectively.
+
+=head2 Flavour-style files
+
+If you want a format change that can't be made by CSS, you can
+override the HTML generated by creating files similar to Blosxom's
+flavour files. They should be named calendar.I<bit>.I<flavour>; for
+available I<bit>s and their default meanings, see the C<__DATA__>
+section in the plugin.
+
+=head1 Installation
+
+1. Download and unpack (if you're reading this, you've probably
+ already done that)
+2. Copy it to your plugins directory. Make sure it's world-readable.
+3. Modify a C<head> or C<foot> file to include C<$calendar::calendar>,
+ C<$calendar::month_calendar> or C<$calendar::year_calendar>
+4. Try it out -- load your blog in your browser. If you see a calendar, great!
+5. Look at your error log. Verify you have an 'enabled' line.
+6. If you're wanting to verify caching is working, load the page
+ again, and now look for an error log line "calendar debug 1: Using
+ cached state"
+7. Once you're satisfied it's working, edit the C<$debug_level> configuration
+ variable to C<0>. There are a couple other configuration variables you may
+ wish to change, too.
+
+=head1 Caching
+
+If the Storable module is available and $use_caching is set, various
+bits of data will be cached; this includes the information on what
+days have stories (and are thus linkable, and what months and years
+are included in the next/forward lists), the contents of any flavour
+files, and the final formatted output of any calendars generated.
+
+The cache will be flushed whenever a story is added (but not when one
+is removed), so in normal use should be invisible. If you're making
+template changes however, or are removing stories, you may wish to
+either disable the cache (by setting $use_caching to 0) or manually
+flush the cache; this can be done by removing
+$plugin_state_dir/.calendar.cache, and is always safe to do.
+
+=head1 LICENSE
+
+this Blosxom Plug-in
+Copyright 2003, Todd Larason
+
+(This license is the same as Blosxom's)
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+
--- /dev/null
+# Blosxom Plugin: categories
+# Author: Todd Larason (jtl@molehill.org)
+# Version: 1.1
+# Blosxom Home/Docs/Licensing: http://www.blosxom.com/
+
+# This plugin is simplified version of "categories" plugin.
+# "categories" plugin maybe found at:
+# http://www.blosxom.com/plugins/category/categories.htm
+
+package categories;
+
+use strict;
+use vars qw($categories $title $name);
+
+# --- Configuration Variables ----------
+
+# should the story-count add up?
+my $add_up_story_count = 1;
+
+# should the story-count display?
+my $display_story_count = 0;
+
+# directories to include, but not children of -- full name under $datadir
+my @prune_dirs = qw!/draft /old /freeze!;
+
+# friendly name of directories
+my %friendly_name = (
+ 'alt-shell' => 'Alternative Shell',
+ 'blog' => 'blog',
+ 'blosxom' => 'blosxom',
+ 'coding' => 'Coding',
+ 'foaf' => 'FOAF',
+ 'gadget' => 'Gadget',
+ 'game' => 'Game',
+ 'internet' => 'Internet',
+ 'media' => 'Media',
+ 'misc' => 'Misc',
+ 'rss' => 'RSS',
+ 'software' => 'Software',
+ 'sports' => 'Sports',
+ 'webdesign' => 'Web Design',
+);
+
+# separator string between $blog_title and $categories::name
+my $title_sep = " - ";
+
+# separater string between each name of category
+my $name_sep = " » ";
+
+# --- Plug-in package variables --------
+
+my %children;
+my %stories;
+my %seen;
+
+# --------------------------------------
+
+sub start {
+ return 1;
+}
+
+sub filter {
+ my($pkg, $files) = @_;
+
+ foreach (keys %$files) {
+ my($dir, $file) = m!(.*)/(.*)!;
+ my $child;
+ $stories{$dir}++;
+
+ while ($dir ne $blosxom::datadir) {
+ ($dir, $child) = ($dir =~ m!(.*)/(.*)!);
+ $stories{$dir}++ if $add_up_story_count;
+
+ if (!$seen{"$dir/$child"}++) {
+ push @{$children{$dir}}, $child;
+ }
+ }
+ }
+
+ $categories = report_root();
+
+ return 1;
+}
+
+sub head {
+ if (!$blosxom::path_info_yr and $blosxom::path_info and
+ ($blosxom::path_info !~ m|.*?/?\w+\.\w+$|)) {
+ $title = '';
+ my @path_info = split(/\//, $blosxom::path_info);
+
+ foreach (@path_info) {
+ next if !$_;
+ $_ = %friendly_name->{$_} if %friendly_name->{$_};
+ $title .= qq!$name_sep$_!;
+ }
+ }
+
+ $title =~ s!^$name_sep!$title_sep!;
+
+ return 1;
+}
+
+sub story {
+ my($pkg, $path, $fn, $story_ref, $title_ref, $body_ref) = @_;
+
+ $name = '';
+ my @name = split(/\//, $path);
+
+ foreach (@name) {
+ next if !$_;
+ $_ = %friendly_name->{$_} if %friendly_name->{$_};
+ $name .= qq!$name_sep$_!;
+ }
+
+ $name =~ s!^$name_sep!!;
+
+ return 1;
+}
+
+sub report_root {
+ my $results = report_categories_start();
+# $results .= report_dir_start('', 'all entries', $stories{$blosxom::datadir});
+
+ foreach (sort @{$children{$blosxom::datadir}}) {
+ $results .= report_dir('/', $_);
+ }
+
+# $results .= report_dir_end();
+ $results .= report_categories_end();
+
+ return $results;
+}
+
+sub report_categories_start {
+ return qq!<ul>\n!;
+}
+
+sub report_dir_start {
+ my($fulldir, $thisdir, $numstories) = @_;
+ $numstories ||= 0;
+ $thisdir = %friendly_name->{$thisdir} if %friendly_name->{$thisdir};
+
+ return qq!<li><a href="$blosxom::url${fulldir}" title="$thisdir">$thisdir</a> ($numstories)\n<ul>\n! if $display_story_count;
+ return qq!<li><a href="$blosxom::url${fulldir}" title="$thisdir">$thisdir</a>\n<ul>\n!;
+}
+
+sub report_dir {
+ my($parent, $dir) = @_;
+ my $results;
+ local $_;
+
+ if (!defined($children{"$blosxom::datadir$parent$dir"}) || is_prune_dir("$parent$dir")) {
+ $results = report_dir_leaf("$parent$dir/", "$dir", $stories{"$blosxom::datadir$parent$dir"});
+ }
+ else {
+ $results = report_dir_start("$parent$dir/", "$dir", $stories{"$blosxom::datadir$parent$dir"});
+
+ foreach (sort @{$children{"$blosxom::datadir$parent$dir"}}) {
+ $results .= report_dir("$parent$dir/", $_);
+ }
+
+ $results .= report_dir_end();
+ }
+
+ return $results;
+}
+
+sub report_dir_leaf {
+ my($fulldir, $thisdir, $numstories) = @_;
+ $numstories ||= 0;
+ $thisdir = %friendly_name->{$thisdir} if %friendly_name->{$thisdir};
+
+ return qq!<li><a href="$blosxom::url${fulldir}" title="$thisdir">$thisdir</a> ($numstories)</li>\n! if $display_story_count;
+ return qq!<li><a href="$blosxom::url${fulldir}" title="$thisdir">$thisdir</a></li>\n!;
+}
+
+sub report_dir_end {
+ return qq!</ul>\n</li>\n!;
+}
+
+sub report_categories_end {
+ return qq!</ul>!;
+}
+
+sub is_prune_dir {
+ my($dir) = @_;
+
+ foreach (@prune_dirs) {
+ return 1 if $dir eq $_;
+ }
+
+ return 0;
+}
+
+1;
--- /dev/null
+# Blosxom Plugin: macros -*- perl -*-
+# Author: Todd Larason (jtl@molehill.org)
+# Version: 0+1i
+# Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
+# Calendar plugin Home/Docs/Licensing:
+# http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/Macros/
+# Modelled on Brad Choate's MT-Macros, but no code in common
+package macros; # -*- perl -*-
+
+# --- Configuration Variables ---
+$macrodir = "$blosxom::plugin_state_dir/.macros"
+ unless defined $macrodir;
+
+$use_caching = 1;
+$debug_level = 1;
+# -------------------------------------------------------------------
+# types:
+# string implemented
+# pattern implemented
+# tag (string or pattern) implemented
+# ctag (string or pattern) implemented
+
+# attributes:
+# name implemented, auto defaults
+# once implemented
+# recurse
+# no_html implemented, default
+# for string and pattern, inhtml => 1 to
+# reverse; can't reverse for tag
+# no_case
+# * defaults for attribs implemented
+# * body implemented
+
+# in replacement:
+# pattern: ${1}-${9} matched () text, $<1>-$<9> escaped matched text
+# tag: as in pattern + ${name} tag attribute, $<name> escaped attribute
+# ctag: as in tag + ${body}
+
+use bytes;
+use File::stat;
+\f
+# XXX cache macro definitions?
+my @macros = ();
+my $cache;
+my $package = "macros";
+my $cachefile = "$blosxom::plugin_state_dir/.$package.cache";
+my $save_cache = 0;
+\f
+sub debug {
+ my ($level, @msg) = @_;
+
+ if ($debug_level >= $level) {
+ print STDERR "$package debug $level: @msg\n";
+ }
+ 1;
+}
+
+sub url_escape {
+ local ($_) = @_;
+
+ s/([^a-zA-Z0-9])/sprintf("%%%02x",ord($1))/eg;
+ s/%20/+/g;
+ return $_;
+}
+\f
+sub define_macro {
+ my ($arg) = @_;
+ my $macro = {};
+
+ if ($arg->{type} eq "string") {
+ $macro->{type} = 'pattern';
+ $macro->{pattern} = qr{\Q$arg->{string}\E};
+ $macro->{body} = $arg->{body};
+ $macro->{inhtml} = $arg->{inhtml} if $arg->{inhtml};
+ $macro->{once} = $arg->{once} if $arg->{once};
+ $macro->{name} = $arg->{name} || "string_$arg->{string}";
+ } elsif ($arg->{type} eq "pattern") {
+ $macro->{type} = 'pattern';
+ $macro->{pattern} = qr{$arg->{pattern}};
+ $macro->{body} = $arg->{body};
+ $macro->{inhtml} = $arg->{inhtml} if $arg->{inhtml};
+ $macro->{once} = $arg->{once} if $arg->{once};
+ $macro->{name} = $arg->{name} || "pattern_$arg->{pattern}";
+ } elsif ($arg->{type} eq "tag") {
+ $macro->{type} = 'tag';
+ $macro->{container} = 0;
+ $macro->{pattern} = qr{$arg->{name}};
+ $macro->{body} = $arg->{body};
+ $macro->{defaults} = {%{$arg->{defaults}}};
+ $macro->{once} = $arg->{once} if $arg->{once};
+ $macro->{name} = "tag_$arg->{name}";
+ } elsif ($arg->{type} eq "ctag") {
+ $macro->{type} = 'tag';
+ $macro->{container} = 1;
+ $macro->{pattern} = qr{$arg->{name}};
+ $macro->{body} = $arg->{body};
+ $macro->{defaults} = {%{$arg->{defaults}}};
+ $macro->{once} = $arg->{once} if $arg->{once};
+ $macro->{name} = "tag_$arg->{name}";
+ }
+
+ push @macros, $macro;
+ return 1;
+}
+
+sub replace_pattern {
+ my ($macro, $ctx) = @_;
+
+ my $replacement = $macro->{body};
+ $replacement =~ s{
+ (?: \$ { ([\w]+) } |
+ \$ < ([\w]+) > |
+ (\$ [\w:]+)
+ )
+ }{defined($1) ? $ctx->{$1} :
+ defined($2) ? url_escape($ctx->{$2}) :
+ eval "$3||''"}xge;
+
+ return $replacement;
+}
+
+sub apply_pattern_macro {
+ my ($state, $macro, $text) = @_;
+ $text =~
+ s{($macro->{pattern})}
+ {$macro->{once} && $state->{used}{$macro->{name}} ? $1 :
+ (++$state->{used}{$macro->{name}}
+ and replace_pattern($macro, {
+ 1 => $2, 2 => $3, 3 => $4,
+ 4 => $5, 5 => $6, 6 => $7,
+ 7 => $8, 8 => $7
+ }))
+ }egms;
+ return $text;
+}
+
+sub apply_tag_macro {
+ my ($state, $macro, $entity, $attributes, $body) = @_;
+ my $ctx;
+
+ $ctx->{body} = $body;
+ $entity =~ $macro->{pattern};
+ @{$ctx}{qw/1 2 3 4 5 6 7 8 9/} = ($1, $2, $3, $4, $5, $6, $7, $8, $9);
+ while ($attributes =~ m{ (\w+) # $1 = tag
+ =
+ (?:
+ " ([^\"]+) " # $2 = quoted value
+ |
+ ([^\s]+) # $3 = unquoted value
+ )
+ }gx) {
+ $ctx->{$1} = ($+);
+ }
+ foreach (keys %{$macro->{defaults}}) {
+ next if defined($ctx->{$_});
+ if ($macro->{defaults}{$_} =~ m:\$(\w+):) {
+ $ctx->{$_} = $ctx->{$1};
+ } else {
+ $ctx->{$_} = $macro->{defaults}{$_};
+ }
+ }
+
+ my $text = $macro->{body};
+ $text =~ s{
+ (?: \$ { ([\w]+) } |
+ \$ < ([\w]+) > |
+ (\$ [\w:]+)
+ )
+ }{defined($1) ? $ctx->{$1} :
+ defined($2) ? url_escape($ctx->{$2}) :
+ eval "$3||''"}xge;
+ return $text;
+}
+
+sub apply_macro {
+ my ($state, $macro, $text) = @_;
+
+ if ($macro->{type} eq 'pattern') {
+ if ($macro->{inhtml}) {
+ $text = apply_pattern_macro($state, $macro, $text);
+ } else {
+ my @tokens = split /(<[^>]+>)/, $text;
+ $text = '';
+ foreach (@tokens) {
+ if (!m/^</) {
+ $_ = apply_pattern_macro($state, $macro, $_);
+ }
+ $text .= $_;
+ }
+ }
+ } elsif ($macro->{type} eq 'tag') {
+ my @tokens = split /(<[^>]+>)/, $text;
+ $text = '';
+ while (defined($_ = shift @tokens)) {
+ if (!($macro->{once} && $state->{used}{$macro->{name}})
+ && (m/<($macro->{pattern})([\s>].*)/)) {
+ my $tag = $_;
+ my $entity = $1;
+ my $attributes = $+;
+ chop $attributes;
+ if ($macro->{container}) {
+ my $body;
+ while (defined($_ = shift @tokens)) {
+ last if (m:</$entity\s*>:);
+ $body .= $_;
+ }
+ $_ = apply_tag_macro($state, $macro, $entity, $attributes, $body);
+ } else {
+ $_ = apply_tag_macro($state, $macro, $entity, $attributes);
+ }
+ $state->{used}{$macro->{name}}++;
+ }
+ $text .= $_;
+ }
+ } else {
+ debug(0, "ACK: unknown macro type $macro->{type}");
+ }
+ return $text;
+}
+
+sub apply_macros {
+ my ($state, $text) = @_;
+
+ foreach my $macro (@macros) {
+ $text = apply_macro($state, $macro, $text);
+ }
+ return $text;
+}
+\f
+# caching support
+
+sub prime_cache {
+ my ($macrokey) = @_;
+ return if (!$use_caching);
+ eval "require Storable";
+ if ($@) {
+ debug(1, "cache disabled, Storable not available");
+ $use_caching = 0;
+ return 0;
+ }
+ if (!Storable->can('lock_retrieve')) {
+ debug(1, "cache disabled, Storable::lock_retrieve not available");
+ $use_caching = 0;
+ return 0;
+ }
+ $cache = (-r $cachefile ? Storable::lock_retrieve($cachefile) : {});
+ if (defined $cache->{macrokey}) {
+ if ($cache->{macrokey} eq $macrokey) {
+ debug(1, "Using restored cache");
+ return 1;
+ }
+ $cache = {};
+ debug(1, "Macros changed, flushing cache");
+ } else {
+ debug(1, "Cache empty, creating");
+ }
+ $cache->{macrokey} = $macrokey;
+ return 0;
+}
+sub save_cache {
+ return if (!$use_caching || !$save_cache);
+ debug(1, "Saving cache");
+ Storable::lock_store($cache, $cachefile);
+}
+\f
+sub story {
+ my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
+ my $state = {};
+ use bytes;
+
+ my $r = $cache->{story}{"$path/$filename"};
+ if ($r && $r->{orig} eq $$body_ref) {
+ $$body_ref = $r->{expanded};
+ return 1;
+ }
+ debug(1, "Cache miss due to story change: $path/$filename") if $r;
+ $cache->{story}{"$path/$filename"}{orig} = $$body_ref;
+ $$body_ref = apply_macros($state, $$body_ref);
+ $cache->{story}{"$path/$filename"}{expanded} = $$body_ref;
+ $save_cache = 1;
+ return 1;
+}
+
+sub start {
+ my $macrokey = '';
+ if (opendir MACROS, $macrodir) {
+ foreach my $macrofile (grep { /^\d*\w+$/ && -f "$macrodir/$_" }
+ sort readdir MACROS) {
+ my $mtime = stat("$macrodir/$macrofile")->mtime;
+ $macrokey .= "$macrofile:$mtime|";
+ require "$macrodir/$macrofile";
+ }
+ }
+ prime_cache($macrokey);
+ return 1;
+}
+
+sub end {
+ save_cache();
+ 1;
+}
+1;
+
+=head1 NAME
+
+Blosxom Plug-in: macros
+
+=head1 SYNOPSIS
+
+Purpose: Generalized macro system modelled on MT-Macros
+
+ * String macros: replace a string with another string
+
+ * Pattern macros: replace a regular-expression pattern with a
+ string optionally based on the replaced text
+
+ * Tag macros: replace html-style content-less tags (like img)
+ (specified with either a string or a pattern) with a string,
+ optionally based on the replaced entity and attributes, with
+ default attributes available
+
+ * Content Tag macros: relace html-style content tags (like a)
+ (specified with either a string or a pattern) with a string,
+ optionally based on the replaced entity, attributes, and
+ contents, with default attributes available
+
+=head1 VERSION
+
+0+1i
+
+1st wide-spread test release
+
+=head1 AUTHOR
+
+Todd Larason <jtl@molehill.org>, http://molelog.molehill.org/
+
+=head1 BUGS
+
+None known; address bug reports and comments to me or to the Blosxom
+mailing list [http://www.yahoogroups.com/groups.blosxom].
+
+=head1 Customization
+
+=head2 Configuration variables
+
+C<$macrodir> is the name of the directory to look for macro definition files
+in; defaults to $plugin_state_dir/.macros. Each file in this directory
+whose name matches /^\d*\w+$/ (that is, optional digits at the beginning,
+followed by letters, numbers and underscores) is read, in order sorted by
+filename. See "Macro Definition" section for details on file contents.
+
+C<$use_caching> controls whether or not to try to cache formatted results;
+caching requires Storable, but the plugin will work just fine (although
+possibly slowly, with lots of macros installed) without it.
+
+C<$debug_level> can be set to a value between 0 and 5; 0 will output
+no debug information, while 5 will be very verbose. The default is 1,
+and should be changed after you've verified the plugin is working
+correctly.
+
+=head2 Macro Definitions
+
+The macro files are simply perl scripts that are read and executed.
+Normally, they consist simply of literal calls to define_macro(), but
+any other perl content is allowed.
+
+As with all perl scripts, loading this script needs to return a true value.
+define_macro() returns 1, so in most cases this will be taken care of
+automatically, but if you're doing something fancy you need to be aware of
+this.
+
+define_macro() takes a single argument, a reference to a hash. The hash
+must contain a 'type' element, which must be one of "string", "pattern",
+"tag" and "ctag". The other elements depend on the type.
+
+=head3 String Macros
+
+To define a string macro, pass define_macros() a hash containing:
+
+ * type => "string", required
+ * string => string, required; the string to be replaced
+ * body => string, required; the string to replace with; no variables are
+ useful, but the same replacement method is used as others, so $ is magic.
+ * inhtml => boolean, optional; if 1, then the string will be replaced even
+ if it appears in the HTML markup; of 0, the string will only be replaced
+ in content. The default is 0 (this is reverse MT-Macros' option, and
+ apparently reverse MT-Macros' default)
+ * once => boolean, optional; if 1, then the string will only be replaced
+ the first time it's seen in a given piece of text (that is, story body).
+ The default is 0.
+ * name => string, optional; currently names aren't used for anything, but
+ they may be in the future.
+
+=head3 Pattern Macros
+
+To define a pattern macro, pass define_macros() a hash containing:
+
+ * type => "pattern", required
+ * pattern => pattern, required; the regular expression to be replaced
+ * body => string, required; the string to replace with; ${1} through ${9}
+ are replaced with the RE match variables $1 through $9; $<1> through $<9>
+ are the same thing, URL encoded.
+ * inhtml => boolean, optional; if 1, then the string will be replaced even
+ if it appears in the HTML markup; of 0, the string will only be replaced
+ in content. The default is 0 (this is reverse MT-Macros' option, and
+ apparently reverse MT-Macros' default). Note that if inhtml is 0, then
+ the pattern is matched against each chunk of content separately, and thus
+ the full pattern must be included in a single markup-less chunk to be
+ seen.
+ * once => boolean, optional; if 1, then the pattern will only be replaced
+ the first time it's seen in a given piece of text (that is, story body).
+ The default is 0.
+ * name => string, optional; currently names aren't used for anything, but
+ they may be in the future.
+
+=head3 Tag Macros
+
+To define a tag macro, pass define_macros() a hash containing:
+
+ * type => "tag", required
+ * pattern => pattern, required; a regular expression matching the entity
+ tag to be replaced; in normal cases this will just be a string, but
+ something like pattern => 'smily(\d+)' could be used to define a whole
+ set of tags like <smily47> at once.
+ * defaults => hashref, optional; a hash reference mapping attribute names
+ to default values. "$\w+" patterns in the default values are replaced
+ the same way "${\w}" patterns in body strings are
+ * body => string, required; the string to replace with; ${1} through ${9}
+ are replaced with the RE match variables $1 through $9; $<1> through $<9>
+ are the same thing, URL encoded. ${attrib} and $<attrib> are replaced with
+ the values of the specified attributes, or with the default for that
+ attribute if the attribute wasn't specified.
+ * once => boolean, optional; if 1, then the tag will only be replaced
+ the first time it's seen in a given piece of text (that is, story body).
+ The default is 0.
+
+=head3 Content Tag Macros
+
+To define a content tag macro, pass define_macros() a hash containing:
+
+ * type => "ctag", required
+ * pattern => pattern, required; a regular expression matching the entity
+ tag to be replaced; in normal cases this will just be a string. The
+ closing tag must exactly match the opening tag, not just match the
+ pattern.
+ * defaults => hashref, optional; a hash reference mapping attribute names
+ to default values. "$\w+" patterns in the default values are replaced
+ the same way "${\w}" patterns in body strings are; in particular, $body
+ can be useful
+ * body => string, required; the string to replace with; ${1} through ${9}
+ are replaced with the RE match variables $1 through $9; $<1> through $<9>
+ are the same thing, URL encoded. ${attrib} and $<attrib> are replaced with
+ the values of the specified attributes, or with the default for that
+ attribute if the attribute wasn't specified. ${body} and $<body> are
+ replaced with the content of the tag.
+ * once => boolean, optional; if 1, then the tag will only be replaced
+ the first time it's seen in a given piece of text (that is, story body).
+ The default is 0.
+
+=head3 examples
+
+=head4 Tatu
+
+This defines a macro that replaces the word "Tatu" with its proper (Cyrllic)
+spelling the first time it's seen in a story; it won't much with markup, so
+URLs containting "Tatu" are safe.
+
+define_macro {
+ type => 'string',
+ string => "Tatu",
+ body => qq!<acronym title=\"Tatu\">Тату</acronym>!,
+ once => 1
+};
+
+This is just like above, but is safer -- it won't match the "Tatu" in
+"Tatuuie".
+
+define_macro {
+ type => 'pattern',
+ pattern => qr/\bTatu\b/,
+ body => qq!<acronym title=\"Tatu\">Тату</acronym>!,
+ once => 1
+};
+
+=head4 Line
+
+This defines a <line> tag with an optional width= attribute
+
+define_macro {
+ type => 'tag',
+ name => 'line',
+ defaults => {width => "100%"},
+ body => '<hr noshade="noshade" width="${width}">'
+};
+
+This can be used either as just <line> or as <line width="50%">.
+
+=head4 Amazon
+
+this defines a fairly fancy <amazon tag
+
+define_macro {
+ type => 'ctag',
+ name => 'amazon',
+ defaults => {domain => 'com', assoc => 'mtmolel-20'},
+ body => '<a href="http://www.amazon.${domain}/exec/obidos/ASIN/${asin}/ref=nosim/${assoc}">${body}</a>'
+};
+
+In normal use, it's something like
+<amazon asin=B00008OE6I>Canon Powershot S400</amazon>
+but it can also be used to refer to something on one of the international
+Amazon sites, like
+on asin=B000089AS9 domain=co.uk>Angel Season 3 DVDs</amazon>
+
+If you wanted to give referral credit to someone else, you could with:
+<amazon asin=B00008OE6I assoc=rael-20>Canon Powershot S400</amazon>
+
+=head4 Google
+
+This defines a <google> tag with a completely optional query attribute; if
+it's not given, then the phrase enclosed by the tag is what's searched for.
+
+define_macro {
+ type => 'ctag',
+ name => 'google',
+ defaults => {query => "\$body"},
+ body => '<a href="http://www.google.com/search?q=$<query>">${body}</a>'
+};
+
+=head4 Programmatic Definitions
+
+There's no reason the macro files need to be literal calls to define_macro.
+
+This example defines its own simplified syntax for defining a set of similar
+macros, reads the definitions, and makmes the appropriate define_macro()
+calls. It's directly translated from a similar MT-Macros definition file,
+(with more macros defined) found at http://diveintomark.org/inc/macros2
+
+while (<DATA>) {
+ chomp;
+ my ($name, $tag, $attrlist) = m/"(.+?)"\s+(\w+)(.*)/;
+ next if !$name;
+ my $attrs = '';
+ my (@attrs) = $attrlist =~ m/\s+(\w+)\s+"(.*?)"/g;
+ for ($i = 0; $i < scalar(@attrs); $i += 2) {
+ my ($attr, $value) = ($attrs[$i], $attrs[$i+1]);
+ $value =~ s/"/"/g; #";
+ $attrs .= qq{ $attr="$value"};
+ }
+ if ($tag =~ /acronym/) {
+ define_macro({
+ name => "abbr_$name",
+ type => pattern,
+ pattern => qr/\b$name\b/,
+ body => "<$tag$attrs>$name</$tag>",
+ once => 1
+ });
+ } elsif ($tag =~ /img/) {
+ define_macro({
+ name => "img_$name",
+ type => string,
+ string => $name,
+ body => "<$tag$attrs>"
+ });
+ } else {
+ define_macro({
+ name => "abbr_$name",
+ type => pattern,
+ pattern => qr/\b$name\b/,
+ body => "<$tag$attrs>$name</$tag>"
+ });
+ }
+}
+
+1;
+__DATA__
+"AOL" acronym title "America Online"
+"API" acronym title "Application Interface"
+"CGI" acronym title "Common Gateway Interface"
+"CMS" acronym title "Content Management System"
+"CSS" acronym title "Cascading Style Sheets"
+"DMV" acronym title "Department of Motor Vehicles"
+":)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20"
+":-)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20"
+"=)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20"
+"=-)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20"
+__END__
+
+=head1 Possible Deficiencies
+
+ * MT-Macros 'recursion' option isn't available. If this is a real problem
+ for you, please let me know, preferably with a good example of what you
+ can't accomplish currently (remember, macros are invoked in the order
+ they're defined, which you can control with filename naming)
+ * tag and ctag macros can't be used in HTML markup. This would be a big
+ problem for Movable Type, where parameter replacement is done with
+ psuedo-HTML, but doesn't seem to be a problem for Blosxom. If it is
+ for you, please let me know, again along with an example.
+ * MT-Macros 'no_case' option isn't available. This can be done by
+ including (?i) in your patterns or defining them with qr//i, instead.
+ * tag and ctag macros can't be explicitely named, because the 'name'
+ parameter is already being used. Future versions may change tag
+ and ctag to use 'string' or 'pattern' for what 'name' is currently
+ used for, and use 'name' to define a macro. That will only be done
+ if there's a good use for names, though.
+ * Once defined, macros are always active. They can't be deactivated on a
+ per-story basis. This might be handled with a meta- header at some
+ point, if someone gives me a reasonable example for why they need it.
+ * There's no built-in data-based macro definition syntax. It's not clear
+ to me that a literal define_macro() call is any more difficult than
+ MT-Macros' HTML-looking (but not HTML-acting) definition syntax, though,
+ and as shown above simpler syntaxes ban be custom-built as appropriate.
+ I'd be more than happy to include a simpler syntax, though, if someone
+ were to develop one that were obviously better than define_syntax().
+
+=head1 Caching
+
+If the Storable module is available and $use_caching is set, formatted
+stories will be cached; the cache is globally keyed off the list of macro
+files and their modification date, and per-story on the contents of the
+story itself. It should thus not ever be necessary to manually flush the
+cache, but it's always safe to do so, by removing the
+$plugin_state_dir/.macros.cache file.
+
+=head1 LICENSE
+
+this Blosxom Plug-in
+Copyright 2003, Todd Larason
+
+(This license is the same as Blosxom's)
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+
--- /dev/null
+# Blosxom Plugin: netflix
+# Author: Todd Larason (jtl@molehill.org)
+# Version: 0+1i
+# Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
+# Netflix plugin Home/Docs/Licensing:
+# http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/Netflix/
+package netflix;
+
+# -------------- Configuration Variables --------------
+
+# Get this from your browser's cookie file. I don't know (yet) how often it
+# will need to be updated
+$ShopperID = undef
+ unless defined $ShopperID;
+
+# how long to go between re-fetching the data, in seconds
+# default value is 1 day
+$max_cache_age = 60 * 60 * 24
+ unless defined $max_cache_age;
+
+$debug_level = 1
+ unless defined $debug_level;
+
+# -----------------------------------------------------
+\f
+$have = '';
+$queue = '';
+\f
+use LWP::UserAgent;
+use HTTP::Cookies;
+use Storable;
+use vars qw/$ShopperID $debug_level $max_cache_age $have $queue/;
+
+use strict;
+\f
+my $cache;
+my $package = "netflix";
+my $cachefile = "$blosxom::plugin_state_dir/.$package.cache";
+my $save_cache = 0;
+my $url = 'http://www.netflix.com/Queue?';
+my $main_re = qr!DVDs\sYou\sHave\sOut
+ (.*)
+ </table> .* DVDs\sin\sYour\sQueue
+ (.*)
+ </table>!xs;
+my $item_re = qr!href="http://www\.netflix\.com/MovieDisplay\?movieid=
+ (\d+)
+ &trkid=\d+">
+ ([^<]+)
+ </a>!xs;
+\f
+sub debug {
+ my ($level, @msg) = @_;
+
+ if ($debug_level >= $level) {
+ print STDERR "$package debug $level: @msg\n";
+ }
+}
+
+sub load_template {
+ my ($bit) = @_;
+ return $blosxom::template->('', "$package.$bit", $blosxom::flavour);
+}
+
+sub report {
+ my ($bit, $list, $id, $title) = @_;
+ my $f = load_template($bit);
+ $f =~ s/((\$[\w:]+)|(\$\{[\w:]+\}))/$1 . "||''"/gee;
+ return $f;
+}
+\f
+sub prime_cache {
+ $cache = (-r $cachefile ? Storable::lock_retrieve($cachefile) : undef);
+ if ($cache && time - $cache->{timestamp} < $max_cache_age) {
+ debug(1, "Using cached state");
+ return 1;
+ }
+ $cache = {timestamp => time};
+ return 0;
+}
+
+sub save_cache {
+ return if (!$save_cache);
+ debug(1, "Saving cache");
+ Storable::lock_store($cache, $cachefile);
+}
+\f
+sub get_page {
+ return $cache->{text} if defined $cache->{text};
+ my $jar = HTTP::Cookies->new;
+ my $req = HTTP::Request->new(GET => $url);
+ my $ua = LWP::UserAgent->new;
+ $jar->set_cookie(0, 'NetflixShopperId', $ShopperID, '/', '.netflix.com');
+ $jar->add_cookie_header($req);
+ my $res = $ua->request($req);
+ if (!$res->is_success) {
+ my $error = $res->status_line;
+ debug(0, "HTTP error: $error");
+ return undef;
+ }
+ $cache->{text} = $res->content;
+
+ # don't set $save_cache, because we only want to save the text
+ # if it's valid & parsable
+
+ return $cache->{text};
+}
+
+sub parse_page {
+ return if $#{$cache->{items}{have}} >= 0;
+ return if $#{$cache->{items}{queue}} >= 0;
+ local ($_) = @_;
+ my ($out_text, $queue_text) = (m/$main_re/);
+ push @{$cache->{items}{have}}, [$1,$2] while ($out_text =~ m/$item_re/g);
+ push @{$cache->{items}{queue}},[$1,$2] while ($queue_text =~ m/$item_re/g);
+}
+
+sub build_list {
+ my ($name, $items) = @_;
+ my $results;
+
+ return $cache->{list}{$name}{$blosxom::flavour}
+ if defined $cache->{list}{$name}{$blosxom::flavour};
+
+ $results = report("head", "$name");
+ $results .= report("item", "$name", $_->[0], $_->[1]) foreach (@$items);
+ $results .= report("foot", "$name");
+
+ $cache->{list}{$name}{$blosxom::flavour} = $results;
+ $save_cache = 1 if $#{$items} >= 0;
+
+ return $results;
+}
+\f
+sub start {
+ return 0 unless defined $netflix::ShopperID;
+ while (<DATA>) {
+ last if /^(__END__)?$/;
+ chomp;
+ my ($flavour, $comp, $txt) = split ' ',$_,3;
+ $txt =~ s:\\n:\n:g;
+ $blosxom::template{$flavour}{"$package.$comp"} = $txt;
+ }
+ return 1;
+}
+
+sub head {
+ prime_cache();
+ my $text = get_page();
+ return 0 unless defined $text;
+ parse_page($text);
+ $have = build_list('have', $cache->{items}{have});
+ $queue = build_list('queue', $cache->{items}{queue});
+ save_cache();
+}
+
+1;
+
+__DATA__
+error head <ul class="netflix $list">\n
+error item <li><a href="http://www.netflix.com/MovieDisplay?movieid=$id">$title</a></li>\n
+error foot </ul>\n
+__END__
+
+=head1 NAME
+
+Blosxom Plug-in: netflix
+
+=head1 SYNOPSIS
+
+Purpose: Lets you easily share your Netflix queue information
+
+ * $netflix::have -- list of DVDs currently checked out (or on the way))
+ * $netflix::queue -- list of DVDs in your queue
+
+=head1 VERSION
+
+0+1i
+
+1st test release
+
+=head1 AUTHOR
+
+Todd Larason <jtl@molehill.org>, http://molelog.molehill.org/
+
+=head1 BUGS
+
+None known; address bug reports and comments to me or to the Blosxom
+mailing list [http://www.yahoogroups.com/groups.blosxom].
+
+=head1 Customization
+
+=head2 Configuration variables
+
+C<$ShopperID> is the key to your Netflix identity; it's a cookie set by the
+login process. If your browser keeps its cookies in the historical Netscape
+format, look in C<cookies.txt> for a line like:
+
+.netflix.com TRUE / FALSE 1050405980 NetflixShopperId P0000000000000000000000445557579205
+
+The long value starting with "P000" is the ShopperID. Until you define this,
+the plugin does nothing at all.
+
+C<$max_cache_age> sets how long to cache the queue information for.
+
+C<$debug_level> can be set to a value between 0 and 5; 0 will output
+no debug information, while 5 will be very verbose. The default is 1,
+and should be changed after you've verified the plugin is working
+correctly.
+
+=head2 Classes for CSS control
+
+There's are some classes used, available for CSS customization.
+
+ * C<netflix> -- both lists are in the netflix class
+ * C<have> -- the 'have' list is also in the have class
+ * C<queue> -- the 'queue' list is also in the queue class
+
+=head2 Flavour-style files
+
+If you want a format change that can't be made by CSS, you can
+override the HTML generated by creating files similar to Blosxom's
+flavour files. They should be named netflix.I<bit>.I<flavour>; for
+available I<bit>s and their default meanings, see the C<__DATA__>
+section in the plugin.
+
+=head1 Caching
+
+Because fetching the queue information is relatively slow, I don't believe
+anyone would want to use it without caching. Thus, this module requires
+Storable, and caching is always on.
+
+=head1 LICENSE
+
+this Blosxom Plug-in
+Copyright 2003, Todd Larason
+
+(This license is the same as Blosxom's)
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+=end
--- /dev/null
+# Blosxom Plugin: postgraph -*- perl -*-
+# Author: Todd Larason jtl@molehill.org and Nilesh Chaudhari http://nilesh.org/
+# Version: 0+1i
+# Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
+# Categories plugin Home/Docs/Licensing:
+# http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/Graph/
+# parts Copyright (c) 2002 Nilesh Chaudhari http://nilesh.org/
+
+package postgraph;
+
+# --- Configuration Variables ---
+$destination_dir ||= ''; # must be configured
+$graph_start_day ||= "19000101";
+$graph_num_bars ||= 24;
+$graph_width ||= 200;
+$graph_height ||= 100;
+$barcolor ||= "#f5deb3"; # the bars themselves
+$bordercolor ||= "#83660f"; # the borders of the bars
+$outlinecolor ||= "#83660f"; # the outline of the box around the graph
+$boxcolor ||= "#fffbf0"; # the inside of the box
+$textcolor ||= "#4f0000"; # the various text bits
+
+$bt_width ||= 400;
+$bt_height ||= 30;
+$bt_linecolor ||= '#ffffff';
+$bt_textcolor ||= '#757575';
+$bt_fillcolor ||= '#757575';
+$bt_bordercolor ||= '#757575';
+$bt_padding = 5 unless defined $bt_padding;
+$bt_show_text = 1 unless defined $bt_show_text;
+
+$debug_level = 2 unless defined $debug_level;
+# ------------------------------------------------------------
+\f
+use File::stat;
+use GD;
+use GD::Graph::bars;
+use GD::Graph::colour qw/:convert :colours/;
+use strict;
+use vars qw/$destination_dir $graph_start_day $graph_num_bars $graph_width
+ $graph_height $barcolor $bordercolor $outlinecolor $boxcolor $textcolor
+ $bt_width $bt_height $bt_linecolor $bt_textcolor $bt_fillcolor
+ $bt_bordercolor $bt_padding $bt_show_text $debug_level/;
+\f
+my $package = "postgraph";
+my $timestamp_file = "$blosxom::plugin_state_dir/.$package.timestamp";
+my $last_timestamp;
+\f
+sub debug {
+ my ($level, @msg) = @_;
+
+ if ($debug_level >= $level) {
+ print STDERR "$package debug $level: @msg\n";
+ }
+}
+\f
+# utility funcs
+sub max {
+ my $max = 0;
+ foreach (@_) {
+ $max = $_ if $_ > $max;
+ }
+ return $max;
+}
+sub round {
+ return int($_[0] + .5);
+}
+sub hex2rgb {
+ my ($hex) = @_;
+
+ my ($r, $g, $b) = ($hex =~ m!\#(..)(..)(..)!);
+ return (hex($r),hex($g),hex($b));
+}
+\f
+my @bucketcount;
+my $secs_per_bucket = 86400 / $graph_num_bars;
+my $mins_per_bucket = 1440 / $graph_num_bars;
+my $buckets_per_hour = $graph_num_bars / 24;
+
+sub graph_add {
+ my ($hour, $min, $sec) = @_;
+ my $time = $hour * 3600 + $min * 60 + $sec;
+ my $bucket = $time / $secs_per_bucket;
+ $bucketcount[$bucket]++;
+}
+
+sub build_graph {
+ my @labels;
+ for (my $hour = 0; $hour < 24; $hour++) {
+ for (my $min; $min < 60; $min += $mins_per_bucket) {
+ push @labels, $hour; # sprintf("%d:%02d", $hour, $min);
+ }
+ }
+ my $graph = GD::Graph::bars->new($graph_width, $graph_height);
+ $graph->set(
+# title => 'Posts per hour of day',
+ y_max_value => max(@bucketcount) + 2,
+ x_label_skip => $buckets_per_hour * 4,
+# bgclr => add_colour($bgcolor), # does nothing?
+ fgclr => add_colour($outlinecolor),
+ boxclr => add_colour($boxcolor),
+ labelclr => add_colour($textcolor),
+ axislabelclr => add_colour($textcolor),
+ legendclr => add_colour($textcolor),
+ valuesclr => add_colour($textcolor),
+ textclr => add_colour($textcolor),
+ dclrs => [add_colour($barcolor)],
+ borderclrs => [add_colour($bordercolor)],
+ );
+ $#bucketcount = $graph_num_bars;
+ my $gd = $graph->plot([\@labels, \@bucketcount]);
+ open IMG, "> $destination_dir/graph.png";
+ binmode IMG;
+ print IMG $gd->png;
+ close IMG;
+ debug(2, "build graph");
+}
+\f
+# blogtimes directly derived from BLOGTIMES by Nilesh Chaudhari
+# -- http://nilesh.org/mt/blogtimes/
+
+my @monthname=('null', 'JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE',
+ 'JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER');
+
+my @bt_entry_times;
+sub bt_add {
+ my ($hour, $min) = @_;
+ push @bt_entry_times, $hour*60 + $min;
+}
+sub build_blogtimes {
+ my ($year, $month) = @_;
+ my $txtpad = $bt_show_text ? gdTinyFont->height : 0;
+ my $scale_width = $bt_width + ($bt_padding*2);
+ my $scale_height = $bt_height + ($bt_padding*2) + ($txtpad*2);
+ my $img = GD::Image->new($scale_width,$scale_height);
+ my $white = $img->colorAllocate(255,255,255);
+ my $linecolor = $img->colorAllocate(hex2rgb($bt_linecolor));
+ my $textcolor = $img->colorAllocate(hex2rgb($bt_textcolor));
+ my $fillcolor = $img->colorAllocate(hex2rgb($bt_fillcolor));
+ my $bordercolor = $img->colorAllocate(hex2rgb($bt_bordercolor));
+ my $line_y1 = $bt_padding + $txtpad;
+ my $line_y2 = $bt_padding + $txtpad + $bt_height;
+ $img->transparent($white);
+ $img->rectangle(0, 0, $scale_width-1, $scale_height-1, $bordercolor);
+ $img->filledRectangle($bt_padding, $line_y1, $bt_padding + $bt_width,
+ $line_y2, $fillcolor);
+ my ($line_x,$i);
+ foreach $i (@bt_entry_times) {
+ $line_x = $bt_padding + (round(($i/1440) * $bt_width));
+ $img->line($line_x, $line_y1, $line_x, $line_y2, $linecolor);
+ }
+ # Shut off text if width is too less.
+ if ($bt_show_text) {
+ if ($bt_width >= 100) {
+ my $ruler_y = $bt_padding + $txtpad + $bt_height + 2;
+ my $ruler_x;
+ for ($i = 0; $i <= 23; $i += 2) {
+ $ruler_x = $bt_padding + round($i * $bt_width/24);
+ $img->string(gdTinyFont,$ruler_x,$ruler_y,"$i",$textcolor);
+ debug(5, 'tinyfont',$ruler_x,$ruler_y,"$i",$textcolor);
+ }
+ $img->string(gdTinyFont, $bt_padding + $bt_width-2,
+ $ruler_y, "0", $textcolor);
+ my $caption_x = $bt_padding;
+ my $caption_y = $bt_padding-1;
+ my $caption = "B L O G T I M E S $monthname[$month] $year";
+ $img->string(gdTinyFont, $caption_x, $caption_y,
+ $caption, $textcolor);
+ debug(5, 'tinyfont', $caption_x, $caption_y, $caption, $textcolor);
+ } else {
+ my $ruler_y = $bt_padding + $txtpad + $bt_height + 2;
+ my $ruler_x;
+ for ($i = 0; $i <= 23; $i += 6) {
+ $ruler_x = $bt_padding + round($i * $bt_width/24);
+ $img->string(gdTinyFont,$ruler_x,$ruler_y,"$i",$textcolor);
+ }
+ $img->string(gdTinyFont, $bt_padding + $bt_width - 2,
+ $ruler_y, "0", $textcolor);
+ my $caption_x = $bt_padding;
+ my $caption_y = $bt_padding-1;
+ my $caption = "$month $year";
+ $img->string(gdTinyFont, $caption_x, $caption_y,
+ $caption, $textcolor);
+ }
+ }
+ open IMG, "> $destination_dir/blogtimes.png" or die "$!";
+ binmode IMG;
+ print IMG $img->png or die "$!";
+ close IMG;
+ debug(2, "build blogtimes");
+}
+\f
+sub filter {
+ my ($pkg, $files) = @_;
+ my $latest_story = (sort {$b <=> $a} values %$files)[0];
+ return 1 if $latest_story <= $last_timestamp;
+ my @now = localtime;
+ my $now_month = $now[4] + 1;
+ my $now_year = $now[5] + 1900;
+
+ debug(1, "updating graph");
+
+ foreach (keys %{$files}) {
+ my @date = localtime($files->{$_});
+ my $mday = $date[3];
+ my $month = $date[4] + 1;
+ my $year = $date[5] + 1900;
+ graph_add($date[2], $date[1], $date[0])
+ if (sprintf("%04d%02d%02d", $year, $month, $mday) ge
+ $graph_start_day);
+ bt_add($date[2], $date[1])
+ if ($year == $now_year and $month == $now_month);
+ }
+ build_graph();
+ build_blogtimes($now_year, $now_month);
+}
+
+sub start {
+ return 0 unless $destination_dir;
+ $last_timestamp = -e $timestamp_file ? stat($timestamp_file)->mtime : 0;
+ my $fh = new FileHandle;
+ if (!$fh->open(">$timestamp_file")) {
+ debug(0, "Couldn't touch timestamp file $timestamp_file");
+ return 0;
+ }
+ $fh->close;
+ debug(1, "$package enabled");
+ return 1;
+}
+1;
+
+=head1 NAME
+
+Blosxom Plug-in: graph
+
+=head1 SYNOPSIS
+
+Purpose: creates graphs showing the time of day stories are posted
+
+Files created:
+ * $destination_dir/graph.png -- bar graph showing number of posts per
+ period of time (default: hour) since $graph_start_day (default: 19000101)
+ * $destination_dir/blogtimes.png -- a vertical-line form of a scatterplot,
+ showing the posting time of all stories posted this month
+
+=head1 VERSION
+
+0+1i
+
+1st test release
+
+=head1 AUTHOR
+
+Todd Larason <jtl@molehill.org>, http://molelog.molehill.org/
+
+portions (the "BLOGTIMES" chart style) based on code by Nilesh Chaudhari;
+see http://nilesh.org/mt/blogtimes/. Nilesh gets credit, but direct bugs
+to Todd Larason.
+
+=head1 BUGS
+
+None known; address bug reports and comments to me or to the Blosxom
+mailing list [http://www.yahoogroups.com/groups.blosxom].
+
+=head1 Customization
+
+=head2 Configuration variables
+
+There are many configuration variables controlling height, width and colors
+of the two graphs; see the configuration variable section for those.
+
+There's also:
+
+C<$destination_dir>, the directory where the output files will be created;
+this must be configured.
+
+C<$graph_start_day>, the earlist date to consider stories from for the bar
+graph. If you've converted from another weblogging package and lost time of
+day information on converted stories, you probably want to set this to when
+you started using Blosxom.
+
+C<$graph_num_bars> is the number of bars to create in the bargraph form;
+if it isn't evenly divisible by 24, things probably act weird -- that isn't
+well tested.
+
+The C<bt_> variables control the "BLOGTIMES" style chart; the others the
+bar graph. For the bargraph, the width and height is the size of the overall
+image; for the blogtimes, it's for the graph portion only -- padding is added
+for a border and optionally for text. One or the other of these is likely to
+change in a future version to provide consistency.
+
+=head1 Caching
+
+The images are only recreated when they appear to be out of date; a timestamp
+file is maintained for this. To force them to be regenerated, remove
+$plugin_state_dir/.postgraph.timestamp.
+
+=head1 LICENSE
+
+this Blosxom Plug-in
+Copyright 2003, Todd Larason
+"BLOGTIME" Portions
+Copyright (c) 2002 Nilesh Chaudhari http://nilesh.org/
+
+(This license is the same as Blosxom's and the original BLOGTIME's)
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+
--- /dev/null
+# Blosxom Plugin: seeerror -*- perl -*-
+# Author: Todd Larason (jtl@molehill.org)
+# Version: 0+1i
+# Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
+# Categories plugin Home/Docs/Licensing:
+# http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/SeeError/
+
+package seeerror;
+
+# ------------------------- Configuration Variables --------------------------
+# Directory to create the temporary files in.
+# Criteria:
+# * It MUST exist
+# * It MUST be writable
+# * It SHOULD be readable outside Blosxom
+$tmp_dir ||= "/tmp";
+
+# Attempt to deal with fatal errors? Should only be enabled if you need it.
+$handle_die = 0
+ unless defined $handle_die;
+# ----------------------------------------------------------------------------
+\f
+use FileHandle;
+use CGI;
+
+my $tmp_file = sprintf "$tmp_dir/bloserr-$$-$^T-%s.txt", CGI::remote_host();
+open STDERR,"> $tmp_file";
+
+if ($handle_die) {
+ $SIG{__DIE__} = sub {
+ print "content-type: text/html\n\n<h1>Possibly-Fatal Error</h1>$@";
+ };
+}
+
+sub start {1;}
+sub end {
+ close STDERR; # force a flush
+ my $fh = new FileHandle;
+ $fh->open("< $tmp_file");
+ my $errors = join '',<$fh>;
+ $fh->close;
+ if ($errors) {
+ $errors =~ s:&:&:g;
+ $errors =~ s:<:<:g;
+ print "<h1>Error Output</h1><pre>$errors</pre>";
+ }
+ unlink($tmp_file);
+ return 1;
+}
+1;
--- /dev/null
+# Blosxom Plugin: seemore -*- perl -*-
+# Author: Todd Larason (jtl@molehill.org)
+# Version: 0+3i
+# Blosxom Home/Docs/Licensing: http://www.raelity.org/blosxom
+# SeeMore plugin Home/Docs/Licensing:
+# http://molelog.molehill.org/blox/Computers/Internet/Web/Blosxom/SeeMore/
+
+package seemore;
+
+# --- Configuration Variables ---
+# regular expression to split on
+$seemore_split ||= qr/\f|<!-- more -->/;
+
+# show the whole artcile on individual article pages? Good for summaries,
+# not so good for spoiler protection
+$more_on_article = 1 unless defined $more_on_article;
+
+$debug_level = 1 unless defined $debug_level;
+# ----------------------------------------------------------------------
+
+use FileHandle;
+use CGI;
+my $package = 'seemore';
+
+sub debug {
+ my ($level, @msg) = @_;
+
+ if ($debug_level >= $level) {
+ print STDERR "$package debug $level: @msg\n";
+ }
+}
+
+sub load_template {
+ my ($bit) = @_;
+ return $blosxom::template->('', "$package.$bit", $blosxom::flavour);
+}
+
+sub report {
+ my ($bit, $path, $fn) = @_;
+
+ my $f = load_template($bit);
+ $f =~ s/((\$[\w:]+)|(\$\{[\w:]+\}))/$1 . "||''"/gee;
+ return $f;
+}
+
+sub show_more_p {
+ return 1 if $more_on_article and $blosxom::path_info =~ m:\.:;
+ return 1 if (CGI::param("seemore"));
+# XXX return 1 if google/&c spider?
+ return 0;
+}
+
+sub start {
+ debug(1, "start() called, enabled");
+ while (<DATA>) {
+ last if /^(__END__)?$/;
+ my ($flavour, $comp, $txt) = split ' ',$_,3;
+ $txt =~ s:\\n:\n:g;
+ $blosxom::template{$flavour}{"$package.$comp"} = $txt;
+ }
+ return 1;
+}
+
+sub story {
+ my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
+
+ debug(2, "story() called");
+ my $more;
+ ($$body_ref, $more) = split $seemore_split, $$body_ref, 2;
+ if ($more) {
+ debug(2, "story() found more");
+ if (show_more_p()) {
+ $$body_ref .= report('divider', $path, $filename) . $more;
+ } else {
+ $$body_ref .= report('showmore', $path, $filename);
+ }
+ }
+ return 1;
+}
+1;
+__DATA__
+error divider <hr class="seemore">\n
+error showmore <p><a href="$blosxom::url$path/$fn.$blosxom::flavour?seemore=y" class="seemore">See more ...</a></p>\n
+rss showmore <p><a href="$blosxom::url$path/$fn.$blosxom::default_flavour?seemore=y" class="seemore">See more ...</a></p>\n
+__END__
+
+=head1 NAME
+
+Blosxom Plug-in: seemore
+
+=head1 SYNOPSIS
+
+Purpose: Allows for long or spoiler-y posts to be split, with a "See more..." link
+
+=head1 VERSION
+
+0+2i
+
+2nd test release
+
+=head1 AUTHOR
+
+Todd Larason <jtl@molehill.org>, http://molelog.molehill.org/
+
+=head1 BUGS
+
+None known; address bug reports and comments to me or to the Blosxom
+mailing list [http://www.yahoogroups.com/groups.blosxom].
+
+=head1 Customization
+
+=head2 Configuration variables
+
+C<$seemore_split> is the regular expression used to find where to
+split stories; the default matches either a form-feed character (as in
+0+1i) or the string "<!-- more -->" (recommended for most peoples'
+use).
+
+C<$more_on_article> controls whether the full article is shown on
+individual article pages, or only on pages with the special 'seemore'
+argument; it defaults to on (0+3i: this is a change of behavior from
+previous versions). Turning this on makes sense if you're using seemore
+to put summaries on a main index paage, but probably not if you're using it
+for spoiler protection.
+
+C<$debug_level> can be set to a value between 0 and 5; 0 will output
+no debug information, while 5 will be very verbose. The default is 1,
+and should be changed after you've verified the plugin is working
+correctly.
+
+=head2 Classes for CSS control
+
+There's a class used, available for CSS customization.
+
+ * C<seemore> -- the <hr> dividing the short version of the story
+ from the rest, in the full-story view, and the <a> for the "See
+ more ..." link in the short view.
+
+=head2 Flavour-style files
+
+If you want a format change that can't be made by CSS, you can
+override the HTML generated by creating files similar to Blosxom's
+flavour files. They should be named seemore.I<bit>.I<flavour>; for
+available I<bit>s and their default meanings, see the C<__DATA__>
+section in the plugin.
+
+=head1 LICENSE
+
+this Blosxom Plug-in
+Copyright 2003, Todd Larason
+
+(This license is the same as Blosxom's)
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+