From 03e69f4317ba6a4b8f0c7dae9813c9d20511b1e9 Mon Sep 17 00:00:00 2001 From: Gavin Carr Date: Tue, 28 Aug 2007 03:18:37 +0000 Subject: [PATCH] Add set of Todd Larason plugins to general. --- general/allconsuming | 449 ++++++++++++++++++++++++++++++ general/autocorrect | 179 ++++++++++++ general/autotrack | 413 +++++++++++++++++++++++++++ general/blogroll | 503 +++++++++++++++++++++++++++++++++ general/calendar | 614 ++++++++++++++++++++++++++++++++++++++++ general/categories | 195 +++++++++++++ general/macros | 650 +++++++++++++++++++++++++++++++++++++++++++ general/netflix | 258 +++++++++++++++++ general/postgraph | 325 ++++++++++++++++++++++ general/seeerror | 50 ++++ general/seemore | 172 ++++++++++++ 11 files changed, 3808 insertions(+) create mode 100644 general/allconsuming create mode 100755 general/autocorrect create mode 100644 general/autotrack create mode 100644 general/blogroll create mode 100644 general/calendar create mode 100755 general/categories create mode 100644 general/macros create mode 100644 general/netflix create mode 100644 general/postgraph create mode 100644 general/seeerror create mode 100644 general/seemore diff --git a/general/allconsuming b/general/allconsuming new file mode 100644 index 0000000..ee99b9a --- /dev/null +++ b/general/allconsuming @@ -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; +# ----------------------------------------------------- + +$purchased = ''; +$reading = ''; +$rereading = ''; +$favorite = ''; +$completed = ''; +$nofinish = ''; + +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/; + +my $cache; +my $package = "allconsuming"; +my $cachefile = "$blosxom::plugin_state_dir/.$package.cache"; +my $save_cache = 0; + +# 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); +} + +# 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"); + } +} + +# 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\
\$VAR1 =(.*)
\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; +} + +# 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); +} + +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; +} + +# 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 () { + 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 \n +error item \n +error foot
$title Book cover$title, $author
+__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 , 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, +C, C, C, C and C 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 -- all lists are in the netflix class + * C, 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.I; for +available Is 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 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 index 0000000..505a580 --- /dev/null +++ b/general/autocorrect @@ -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 () { + 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

The file you asked for doesn't exist, and I'm afraid I couldn't find a good match for it. I'm sorry.

\n +html found_one_title $text +html found_one_body

The file you asked for doesn't exist; it may have been moved. This is actually $path.


$text +html error_title Error +html error_body

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

+html multi_title Possible Matches +html multi_body_head

The file you asked for doesn't exist. Some possible matches are:

    \n +html multi_body_item
  • $path
  • \n +html multi_body_foot
\n +__END__ diff --git a/general/autotrack b/general/autotrack new file mode 100644 index 0000000..031e5f6 --- /dev/null +++ b/general/autotrack @@ -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; + +# ----------------------------------------------------- + +# template-visible vars + +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/; + +my $dont_really_ping = 0; +my $package = "autotrack"; +my $timestamp_file = "$blosxom::plugin_state_dir/.$package.timestamp"; +my $last_timestamp; +my $files; + +sub debug { + my ($level, @msg) = @_; + + if ($debug_level >= $level) { + print STDERR "$package debug $level: @msg\n"; + } +} + +# utility funcs +sub url_escape { + local ($_) = @_; + + s/([^a-zA-Z0-9])/sprintf("%%%02x",ord($1))/eg; + s/%20/+/g; + return $_; +} + +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 "0"; # 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 "0"; # 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"); + } +} + +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!()!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!()!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:(.*?):ms) { + if ($1) { + my $errcode = $1; + $txt =~ m:(.*):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; +} + +# 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!]* + 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 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 index 0000000..621eb09 --- /dev/null +++ b/general/blogroll @@ -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 (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); +} + +# 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; +} + +# 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; +} + +# 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 index 0000000..b41b1ca --- /dev/null +++ b/general/calendar @@ -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; + +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'); +} + + +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); +} + +sub start { + debug(1, "start() called, enabled"); + + while (<DATA>) { + chomp; + last if /^(__END__)?$/; + my ($flavour, $comp, $txt) = split ' ',$_,3; + $txt =~ s:\\n:\n:g; + $blosxom::template{$flavour}{"$package.$comp"} = $txt; + } + return 1; +} + +sub filter { + my ($pkg, $files_ref) = @_; + debug(1, "filter() called"); + + $files = $files_ref; + + my $num_files = scalar keys %$files; + my @latest = (sort {$b <=> $a} values %$files); + while ($latest[0] == $^T) { + $num_files--; + shift @latest; + } + return 1 if prime_cache($num_files); + + debug(1, "cache miss: %stories"); + + foreach (keys %{$files}) { + next if ($_ == $^T); + my @date = localtime($files->{$_}); + my $mday = $date[3]; + my $month = $date[4] + 1; + my $year = $date[5] + 1900; + $cache->{stories}{"$year"}++; + $cache->{stories}{"$year/$month"}++; + $cache->{stories}{"$year/$month/$mday"}++; + } + debug(1, "filter() done"); + return 1; +} + +sub head { + debug(1, "head() called"); + my ($year, $month, $day) = pseudo_now(); + if ($year < 1970) { + debug(0,"Bad year $year requested ($year, path_info $ENV{PATH_INFO}, year to 2000"); + $year = 2000; + } + $prev_month_link = cached(\&build_prev_month_link, "pml", $year,$month); + $next_month_link = cached(\&build_next_month_link, "nml", $year,$month); + $prev_year_link = cached(\&build_prev_year_link, "pyl", $year); + $next_year_link = cached(\&build_next_year_link, "nyl", $year); + $month_calendar = cached(\&build_month_calendar, "mc", $year,$month,$day); + $year_calendar = ''; + for (my $y = (localtime)[5]+1900; $cache->{stories}{$y}; $y--) { + my $varname = "year_calendar_$y"; + if ($y == $year) { + $year_calendar = cached(\&build_year_calendar, "yc",$year,$month); + $$varname = $year_calendar; + } else { + $$varname = cached(\&build_year_calendar, "yc", $y); + } + } + $year_calendar = cached(\&build_year_calendar, "yc", $year,$month) + unless $year_calendar; + $calendar = cached(\&build_calendar, "c", $year,$month,$day); + + save_cache(); + + debug(1, "head() done, length(\$month_calendar, \$year_calendar, \$calendar) = ", length($month_calendar), length($year_calendar), length($calendar)); + return 1; +} + +# these look better (to me), but don't use <caption> like they 'should', and +# the year one assumes 3 columns +#html month_head <table class="month-calendar"><tr class="month-calendar-head"><th align="left">$prev_month_link</th><th colspan="5"><a title="$monthname $year" href="$url">$monthname</a></th><th align="right">$next_month_link</th></tr>\n +#html year_head <table class="year-calendar"><tr class="year-calendar-head"><th align="left">$prev_year_link</th><th><a title="$year" href="$url">$year</a></th><th align="right">$next_year_link</th></tr><tr><th class="year-calendar-subhead" colspan=$months_per_row>Months</th></tr>\n + +1; +__DATA__ +error month_head <table class="month-calendar"><caption class="month-calendar-head">$prev_month_link<a title="$monthname $year ($count)" href="$url">$monthname</a>$next_month_link</caption>\n +error month_sub_head <tr>\n +error month_sub_day <th class="month-calendar-day-head $downame">$dowabbr</th>\n +error month_sub_foot </tr>\n +error week_head <tr>\n +error noday <td class="month-calendar-day-noday $downame"> </td>\n +error day_link <td class="month-calendar-day-link $downame"><a title="$downame, $day $monthname $year ($count)" href="$url">$day</a></td>\n +error day_nolink <td class="month-calendar-day-nolink $downame">$day</td>\n +error day_future <td class="month-calendar-day-future $downame">$day</td>\n +error this_day_link <td class="month-calendar-day-this-day $downame"><a title="$downame, $day $monthname $year (current) ($count)" href="$url">$day</a></td>\n +error this_day_nolink <td class="month-calendar-day-this-day $downame">$day</td>\n +error week_foot </tr>\n +error month_foot </table>\n +error prev_month_link <a title="$monthname $year ($count)" href="$url">←</a> +error next_month_link <a title="$monthname $year ($count)" href="$url">→</a> +error prev_month_nolink ← +error next_month_nolink → +error year_head <table class="year-calendar"><caption class="year-calendar-head">$prev_year_link<a title="$year ($count)" href="$url">$year</a>$next_year_link</caption><tr><th class="year-calendar-subhead" colspan="$months_per_row">Months</th></tr>\n +error quarter_head <tr>\n +error month_link <td class="year-calendar-month-link"><a title="$monthname $year ($count)" href="$url">$monthabbr</a></td>\n +error month_nolink <td class="year-calendar-month-nolink">$monthabbr</td>\n +error month_future <td class="year-calendar-month-future">$monthabbr</td>\n +error this_month_link <td class="year-calendar-this-month"><a title="$monthname $year ($count)" href="$url">$monthabbr</a></td> +error this_month_nolink <td class="year-calendar-this-month">$monthabbr</td> +error quarter_foot </tr>\n +error year_foot </table>\n +error prev_year_link <a title="$year ($count)" href="$url">←</a> +error next_year_link <a title="$year ($count)" href="$url">→</a> +error prev_year_nolink ← +error next_year_nolink → +error calendar <div class="calendar"><table><tr><td>$calendar::month_calendar</td><td>$calendar::year_calendar</td></tr></table></div> +__END__ + +=head1 NAME + +Blosxom Plug-in: calendar + +=head1 SYNOPSIS + +Purpose: Provides a Radio-style archive navigation calendar + + * $calendar::calendar -- side-by-side month and year calendars + * $calendar::month_calendar -- month calendar only + * $calendar::year_calendar -- year calendar only + * $calendar::year_calendar_2003 -- year calendar for 2003; these are built for every year with stories. + +=head1 VERSION + +0+6i + +6th test release + +=head1 AUTHOR + +Todd Larason <jtl@molehill.org>, http://molelog.molehill.org/ + +=head1 BUGS + +None known; address bug reports and comments to me or to the Blosxom +mailing list [http://www.yahoogroups.com/groups.blosxom]. + +=head1 Customization + +=head2 Configuration variables + +C<@monthname>, C<@monthabbr>, C<@downame> and C<@dowabbr> contain the +long and short forms of the names of the months and days of the week; +@downame and @dowabbr should always start with Sunday, regardless of +$first_dow. + +C<$first_dow> sets the plugin's idea of the first day day of the week; +use 0 for Sunday, 1 for Monday, and so on; using a number outside the +range [0..6] will cause undefined behavior, possibly including a +nuclear meltdown. + +C<$use_caching> controls whether or not to try to cache statistics and +formatted results; caching requires Storable, but the plugin will work +just fine without it. + +C<$months_per_row> controls how many months are on each row of the +year calendar. This should be a number that evenly divides 12 (1, 2, +3, 4, 6 or 12), but nothing checks for that. + +C<$debug_level> can be set to a value between 0 and 5; 0 will output +no debug information, while 5 will be very verbose. The default is 1, +and should be changed after you've verified the plugin is working +correctly. + +=head2 Classes for CSS control + +There's an (over)abundance of classes used, available for CSS customization. + + * C<calendar> -- the calendar as a whole + * C<month-calendar> -- the month calendar as a whole + * C<month-calendar-head> -- the head of the month calendar (ie, + "March") + * C<month-calendar-day-head> -- a column head in the month calendar + (ie, a day-of-week abbreviation) + * C<month-calendar-day-noday>, C<month-calendar-day-link>, + C<month-calendar-day-nolink>, C<month-calendar-day-future>, + C<month-calendar-day-this-day> -- the day squares on the month + calendar, for days that don't exist (before or after the month + itself), that don't have stories, that do have stories, that are + in the future, or are that currently selected, respectively + * Day-of-week-name -- each day square is also given a class matching + its day of week, from C<@downame>; this can be used to hilight + weekends + * C<year-calendar> -- the year calendar as a whole + * C<year-calendar-head> -- the head of the year calendar (ie, + "2003") + * C<year-calendar-subhead> -- ie, "Months" + * C<year-calendar-month-link>, C<year-calendar-month-nolink>, + C<year-calendar-month-future>, C<year-calendar-this-month> -- the + month squares on the year calendar, for months with stories, + without, in the future, and currently selected, respectively. + +=head2 Flavour-style files + +If you want a format change that can't be made by CSS, you can +override the HTML generated by creating files similar to Blosxom's +flavour files. They should be named calendar.I<bit>.I<flavour>; for +available I<bit>s and their default meanings, see the C<__DATA__> +section in the plugin. + +=head1 Installation + +1. Download and unpack (if you're reading this, you've probably + already done that) +2. Copy it to your plugins directory. Make sure it's world-readable. +3. Modify a C<head> or C<foot> file to include C<$calendar::calendar>, + C<$calendar::month_calendar> or C<$calendar::year_calendar> +4. Try it out -- load your blog in your browser. If you see a calendar, great! +5. Look at your error log. Verify you have an 'enabled' line. +6. If you're wanting to verify caching is working, load the page + again, and now look for an error log line "calendar debug 1: Using + cached state" +7. Once you're satisfied it's working, edit the C<$debug_level> configuration + variable to C<0>. There are a couple other configuration variables you may + wish to change, too. + +=head1 Caching + +If the Storable module is available and $use_caching is set, various +bits of data will be cached; this includes the information on what +days have stories (and are thus linkable, and what months and years +are included in the next/forward lists), the contents of any flavour +files, and the final formatted output of any calendars generated. + +The cache will be flushed whenever a story is added (but not when one +is removed), so in normal use should be invisible. If you're making +template changes however, or are removing stories, you may wish to +either disable the cache (by setting $use_caching to 0) or manually +flush the cache; this can be done by removing +$plugin_state_dir/.calendar.cache, and is always safe to do. + +=head1 LICENSE + +this Blosxom Plug-in +Copyright 2003, Todd Larason + +(This license is the same as Blosxom's) + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + diff --git a/general/categories b/general/categories new file mode 100755 index 0000000..c0ce484 --- /dev/null +++ b/general/categories @@ -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 = " » "; + +# --- 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 index 0000000..cdf3f9c --- /dev/null +++ b/general/macros @@ -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; + +# XXX cache macro definitions? +my @macros = (); +my $cache; +my $package = "macros"; +my $cachefile = "$blosxom::plugin_state_dir/.$package.cache"; +my $save_cache = 0; + +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 $_; +} + +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; +} + +# 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); +} + +sub story { + my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_; + my $state = {}; + use bytes; + + my $r = $cache->{story}{"$path/$filename"}; + if ($r && $r->{orig} eq $$body_ref) { + $$body_ref = $r->{expanded}; + return 1; + } + debug(1, "Cache miss due to story change: $path/$filename") if $r; + $cache->{story}{"$path/$filename"}{orig} = $$body_ref; + $$body_ref = apply_macros($state, $$body_ref); + $cache->{story}{"$path/$filename"}{expanded} = $$body_ref; + $save_cache = 1; + return 1; +} + +sub start { + my $macrokey = ''; + if (opendir MACROS, $macrodir) { + foreach my $macrofile (grep { /^\d*\w+$/ && -f "$macrodir/$_" } + sort readdir MACROS) { + my $mtime = stat("$macrodir/$macrofile")->mtime; + $macrokey .= "$macrofile:$mtime|"; + require "$macrodir/$macrofile"; + } + } + prime_cache($macrokey); + return 1; +} + +sub end { + save_cache(); + 1; +} +1; + +=head1 NAME + +Blosxom Plug-in: macros + +=head1 SYNOPSIS + +Purpose: Generalized macro system modelled on MT-Macros + + * String macros: replace a string with another string + + * Pattern macros: replace a regular-expression pattern with a + string optionally based on the replaced text + + * Tag macros: replace html-style content-less tags (like img) + (specified with either a string or a pattern) with a string, + optionally based on the replaced entity and attributes, with + default attributes available + + * Content Tag macros: relace html-style content tags (like a) + (specified with either a string or a pattern) with a string, + optionally based on the replaced entity, attributes, and + contents, with default attributes available + +=head1 VERSION + +0+1i + +1st wide-spread test release + +=head1 AUTHOR + +Todd Larason <jtl@molehill.org>, http://molelog.molehill.org/ + +=head1 BUGS + +None known; address bug reports and comments to me or to the Blosxom +mailing list [http://www.yahoogroups.com/groups.blosxom]. + +=head1 Customization + +=head2 Configuration variables + +C<$macrodir> is the name of the directory to look for macro definition files +in; defaults to $plugin_state_dir/.macros. Each file in this directory +whose name matches /^\d*\w+$/ (that is, optional digits at the beginning, +followed by letters, numbers and underscores) is read, in order sorted by +filename. See "Macro Definition" section for details on file contents. + +C<$use_caching> controls whether or not to try to cache formatted results; +caching requires Storable, but the plugin will work just fine (although +possibly slowly, with lots of macros installed) without it. + +C<$debug_level> can be set to a value between 0 and 5; 0 will output +no debug information, while 5 will be very verbose. The default is 1, +and should be changed after you've verified the plugin is working +correctly. + +=head2 Macro Definitions + +The macro files are simply perl scripts that are read and executed. +Normally, they consist simply of literal calls to define_macro(), but +any other perl content is allowed. + +As with all perl scripts, loading this script needs to return a true value. +define_macro() returns 1, so in most cases this will be taken care of +automatically, but if you're doing something fancy you need to be aware of +this. + +define_macro() takes a single argument, a reference to a hash. The hash +must contain a 'type' element, which must be one of "string", "pattern", +"tag" and "ctag". The other elements depend on the type. + +=head3 String Macros + +To define a string macro, pass define_macros() a hash containing: + + * type => "string", required + * string => string, required; the string to be replaced + * body => string, required; the string to replace with; no variables are + useful, but the same replacement method is used as others, so $ is magic. + * inhtml => boolean, optional; if 1, then the string will be replaced even + if it appears in the HTML markup; of 0, the string will only be replaced + in content. The default is 0 (this is reverse MT-Macros' option, and + apparently reverse MT-Macros' default) + * once => boolean, optional; if 1, then the string will only be replaced + the first time it's seen in a given piece of text (that is, story body). + The default is 0. + * name => string, optional; currently names aren't used for anything, but + they may be in the future. + +=head3 Pattern Macros + +To define a pattern macro, pass define_macros() a hash containing: + + * type => "pattern", required + * pattern => pattern, required; the regular expression to be replaced + * body => string, required; the string to replace with; ${1} through ${9} + are replaced with the RE match variables $1 through $9; $<1> through $<9> + are the same thing, URL encoded. + * inhtml => boolean, optional; if 1, then the string will be replaced even + if it appears in the HTML markup; of 0, the string will only be replaced + in content. The default is 0 (this is reverse MT-Macros' option, and + apparently reverse MT-Macros' default). Note that if inhtml is 0, then + the pattern is matched against each chunk of content separately, and thus + the full pattern must be included in a single markup-less chunk to be + seen. + * once => boolean, optional; if 1, then the pattern will only be replaced + the first time it's seen in a given piece of text (that is, story body). + The default is 0. + * name => string, optional; currently names aren't used for anything, but + they may be in the future. + +=head3 Tag Macros + +To define a tag macro, pass define_macros() a hash containing: + + * type => "tag", required + * pattern => pattern, required; a regular expression matching the entity + tag to be replaced; in normal cases this will just be a string, but + something like pattern => 'smily(\d+)' could be used to define a whole + set of tags like <smily47> at once. + * defaults => hashref, optional; a hash reference mapping attribute names + to default values. "$\w+" patterns in the default values are replaced + the same way "${\w}" patterns in body strings are + * body => string, required; the string to replace with; ${1} through ${9} + are replaced with the RE match variables $1 through $9; $<1> through $<9> + are the same thing, URL encoded. ${attrib} and $<attrib> are replaced with + the values of the specified attributes, or with the default for that + attribute if the attribute wasn't specified. + * once => boolean, optional; if 1, then the tag will only be replaced + the first time it's seen in a given piece of text (that is, story body). + The default is 0. + +=head3 Content Tag Macros + +To define a content tag macro, pass define_macros() a hash containing: + + * type => "ctag", required + * pattern => pattern, required; a regular expression matching the entity + tag to be replaced; in normal cases this will just be a string. The + closing tag must exactly match the opening tag, not just match the + pattern. + * defaults => hashref, optional; a hash reference mapping attribute names + to default values. "$\w+" patterns in the default values are replaced + the same way "${\w}" patterns in body strings are; in particular, $body + can be useful + * body => string, required; the string to replace with; ${1} through ${9} + are replaced with the RE match variables $1 through $9; $<1> through $<9> + are the same thing, URL encoded. ${attrib} and $<attrib> are replaced with + the values of the specified attributes, or with the default for that + attribute if the attribute wasn't specified. ${body} and $<body> are + replaced with the content of the tag. + * once => boolean, optional; if 1, then the tag will only be replaced + the first time it's seen in a given piece of text (that is, story body). + The default is 0. + +=head3 examples + +=head4 Tatu + +This defines a macro that replaces the word "Tatu" with its proper (Cyrllic) +spelling the first time it's seen in a story; it won't much with markup, so +URLs containting "Tatu" are safe. + +define_macro { + type => 'string', + string => "Tatu", + body => qq!<acronym title=\"Tatu\">Тату</acronym>!, + once => 1 +}; + +This is just like above, but is safer -- it won't match the "Tatu" in +"Tatuuie". + +define_macro { + type => 'pattern', + pattern => qr/\bTatu\b/, + body => qq!<acronym title=\"Tatu\">Тату</acronym>!, + once => 1 +}; + +=head4 Line + +This defines a <line> tag with an optional width= attribute + +define_macro { + type => 'tag', + name => 'line', + defaults => {width => "100%"}, + body => '<hr noshade="noshade" width="${width}">' +}; + +This can be used either as just <line> or as <line width="50%">. + +=head4 Amazon + +this defines a fairly fancy <amazon tag + +define_macro { + type => 'ctag', + name => 'amazon', + defaults => {domain => 'com', assoc => 'mtmolel-20'}, + body => '<a href="http://www.amazon.${domain}/exec/obidos/ASIN/${asin}/ref=nosim/${assoc}">${body}</a>' +}; + +In normal use, it's something like +<amazon asin=B00008OE6I>Canon Powershot S400</amazon> +but it can also be used to refer to something on one of the international +Amazon sites, like +on asin=B000089AS9 domain=co.uk>Angel Season 3 DVDs</amazon> + +If you wanted to give referral credit to someone else, you could with: +<amazon asin=B00008OE6I assoc=rael-20>Canon Powershot S400</amazon> + +=head4 Google + +This defines a <google> tag with a completely optional query attribute; if +it's not given, then the phrase enclosed by the tag is what's searched for. + +define_macro { + type => 'ctag', + name => 'google', + defaults => {query => "\$body"}, + body => '<a href="http://www.google.com/search?q=$<query>">${body}</a>' +}; + +=head4 Programmatic Definitions + +There's no reason the macro files need to be literal calls to define_macro. + +This example defines its own simplified syntax for defining a set of similar +macros, reads the definitions, and makmes the appropriate define_macro() +calls. It's directly translated from a similar MT-Macros definition file, +(with more macros defined) found at http://diveintomark.org/inc/macros2 + +while (<DATA>) { + chomp; + my ($name, $tag, $attrlist) = m/"(.+?)"\s+(\w+)(.*)/; + next if !$name; + my $attrs = ''; + my (@attrs) = $attrlist =~ m/\s+(\w+)\s+"(.*?)"/g; + for ($i = 0; $i < scalar(@attrs); $i += 2) { + my ($attr, $value) = ($attrs[$i], $attrs[$i+1]); + $value =~ s/"/"/g; #"; + $attrs .= qq{ $attr="$value"}; + } + if ($tag =~ /acronym/) { + define_macro({ + name => "abbr_$name", + type => pattern, + pattern => qr/\b$name\b/, + body => "<$tag$attrs>$name</$tag>", + once => 1 + }); + } elsif ($tag =~ /img/) { + define_macro({ + name => "img_$name", + type => string, + string => $name, + body => "<$tag$attrs>" + }); + } else { + define_macro({ + name => "abbr_$name", + type => pattern, + pattern => qr/\b$name\b/, + body => "<$tag$attrs>$name</$tag>" + }); + } +} + +1; +__DATA__ +"AOL" acronym title "America Online" +"API" acronym title "Application Interface" +"CGI" acronym title "Common Gateway Interface" +"CMS" acronym title "Content Management System" +"CSS" acronym title "Cascading Style Sheets" +"DMV" acronym title "Department of Motor Vehicles" +":)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20" +":-)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20" +"=)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20" +"=-)" img alt "[smiley face]" title "" src "/images/smilies/smile.gif" width "20" height "20" +__END__ + +=head1 Possible Deficiencies + + * MT-Macros 'recursion' option isn't available. If this is a real problem + for you, please let me know, preferably with a good example of what you + can't accomplish currently (remember, macros are invoked in the order + they're defined, which you can control with filename naming) + * tag and ctag macros can't be used in HTML markup. This would be a big + problem for Movable Type, where parameter replacement is done with + psuedo-HTML, but doesn't seem to be a problem for Blosxom. If it is + for you, please let me know, again along with an example. + * MT-Macros 'no_case' option isn't available. This can be done by + including (?i) in your patterns or defining them with qr//i, instead. + * tag and ctag macros can't be explicitely named, because the 'name' + parameter is already being used. Future versions may change tag + and ctag to use 'string' or 'pattern' for what 'name' is currently + used for, and use 'name' to define a macro. That will only be done + if there's a good use for names, though. + * Once defined, macros are always active. They can't be deactivated on a + per-story basis. This might be handled with a meta- header at some + point, if someone gives me a reasonable example for why they need it. + * There's no built-in data-based macro definition syntax. It's not clear + to me that a literal define_macro() call is any more difficult than + MT-Macros' HTML-looking (but not HTML-acting) definition syntax, though, + and as shown above simpler syntaxes ban be custom-built as appropriate. + I'd be more than happy to include a simpler syntax, though, if someone + were to develop one that were obviously better than define_syntax(). + +=head1 Caching + +If the Storable module is available and $use_caching is set, formatted +stories will be cached; the cache is globally keyed off the list of macro +files and their modification date, and per-story on the contents of the +story itself. It should thus not ever be necessary to manually flush the +cache, but it's always safe to do so, by removing the +$plugin_state_dir/.macros.cache file. + +=head1 LICENSE + +this Blosxom Plug-in +Copyright 2003, Todd Larason + +(This license is the same as Blosxom's) + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + diff --git a/general/netflix b/general/netflix new file mode 100644 index 0000000..d977e93 --- /dev/null +++ b/general/netflix @@ -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; + +# ----------------------------------------------------- + +$have = ''; +$queue = ''; + +use LWP::UserAgent; +use HTTP::Cookies; +use Storable; +use vars qw/$ShopperID $debug_level $max_cache_age $have $queue/; + +use strict; + +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; + +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; +} + +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); +} + +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; +} + +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 index 0000000..697df9c --- /dev/null +++ b/general/postgraph @@ -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; +# ------------------------------------------------------------ + +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/; + +my $package = "postgraph"; +my $timestamp_file = "$blosxom::plugin_state_dir/.$package.timestamp"; +my $last_timestamp; + +sub debug { + my ($level, @msg) = @_; + + if ($debug_level >= $level) { + print STDERR "$package debug $level: @msg\n"; + } +} + +# 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)); +} + +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"); +} + +# 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"); +} + +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 index 0000000..a6d357d --- /dev/null +++ b/general/seeerror @@ -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; +# ---------------------------------------------------------------------------- + +use FileHandle; +use CGI; + +my $tmp_file = sprintf "$tmp_dir/bloserr-$$-$^T-%s.txt", CGI::remote_host(); +open STDERR,"> $tmp_file"; + +if ($handle_die) { + $SIG{__DIE__} = sub { + print "content-type: text/html\n\n<h1>Possibly-Fatal Error</h1>$@"; + }; +} + +sub start {1;} +sub end { + close STDERR; # force a flush + my $fh = new FileHandle; + $fh->open("< $tmp_file"); + my $errors = join '',<$fh>; + $fh->close; + if ($errors) { + $errors =~ s:&:&:g; + $errors =~ s:<:<:g; + print "<h1>Error Output</h1><pre>$errors</pre>"; + } + unlink($tmp_file); + return 1; +} +1; diff --git a/general/seemore b/general/seemore new file mode 100644 index 0000000..2819509 --- /dev/null +++ b/general/seemore @@ -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. + + -- 2.30.2