Add set of Todd Larason plugins to general.
authorGavin Carr <gonzai@users.sourceforge.net>
Tue, 28 Aug 2007 03:18:37 +0000 (03:18 +0000)
committerGavin Carr <gonzai@users.sourceforge.net>
Tue, 28 Aug 2007 03:18:37 +0000 (03:18 +0000)
general/allconsuming [new file with mode: 0644]
general/autocorrect [new file with mode: 0755]
general/autotrack [new file with mode: 0644]
general/blogroll [new file with mode: 0644]
general/calendar [new file with mode: 0644]
general/categories [new file with mode: 0755]
general/macros [new file with mode: 0644]
general/netflix [new file with mode: 0644]
general/postgraph [new file with mode: 0644]
general/seeerror [new file with mode: 0644]
general/seemore [new file with mode: 0644]

diff --git a/general/allconsuming b/general/allconsuming
new file mode 100644 (file)
index 0000000..ee99b9a
--- /dev/null
@@ -0,0 +1,449 @@
+# 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
diff --git a/general/autocorrect b/general/autocorrect
new file mode 100755 (executable)
index 0000000..505a580
--- /dev/null
@@ -0,0 +1,179 @@
+# 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__
diff --git a/general/autotrack b/general/autotrack
new file mode 100644 (file)
index 0000000..031e5f6
--- /dev/null
@@ -0,0 +1,413 @@
+# 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
+
diff --git a/general/blogroll b/general/blogroll
new file mode 100644 (file)
index 0000000..621eb09
--- /dev/null
@@ -0,0 +1,503 @@
+# 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.
+
+
diff --git a/general/calendar b/general/calendar
new file mode 100644 (file)
index 0000000..b41b1ca
--- /dev/null
@@ -0,0 +1,614 @@
+# 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">&nbsp;</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">&larr;</a>
+error next_month_link <a title="$monthname $year ($count)" href="$url">&rarr;</a>
+error prev_month_nolink &larr;
+error next_month_nolink &rarr;
+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">&larr;</a>
+error next_year_link <a title="$year ($count)" href="$url">&rarr;</a>
+error prev_year_nolink &larr;
+error next_year_nolink &rarr;
+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.
+
+
diff --git a/general/categories b/general/categories
new file mode 100755 (executable)
index 0000000..c0ce484
--- /dev/null
@@ -0,0 +1,195 @@
+# 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 = " &#187; ";
+
+# --- 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;
diff --git a/general/macros b/general/macros
new file mode 100644 (file)
index 0000000..cdf3f9c
--- /dev/null
@@ -0,0 +1,650 @@
+# 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\">&#x0422;&#x0430;&#x0442;&#x0443;</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\">&#x0422;&#x0430;&#x0442;&#x0443;</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/"/&quot;/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.
+
+
diff --git a/general/netflix b/general/netflix
new file mode 100644 (file)
index 0000000..d977e93
--- /dev/null
@@ -0,0 +1,258 @@
+# 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
diff --git a/general/postgraph b/general/postgraph
new file mode 100644 (file)
index 0000000..697df9c
--- /dev/null
@@ -0,0 +1,325 @@
+# 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.
+
+
diff --git a/general/seeerror b/general/seeerror
new file mode 100644 (file)
index 0000000..a6d357d
--- /dev/null
@@ -0,0 +1,50 @@
+# 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:&:&amp;:g;
+       $errors =~ s:<:&lt;:g;
+       print "<h1>Error Output</h1><pre>$errors</pre>";
+    }
+    unlink($tmp_file);
+    return 1;
+}
+1;
diff --git a/general/seemore b/general/seemore
new file mode 100644 (file)
index 0000000..2819509
--- /dev/null
@@ -0,0 +1,172 @@
+# 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.
+
+