From e1adedc9d0a948a755ac648befbf5a3415072e5a Mon Sep 17 00:00:00 2001 From: Gavin Carr Date: Wed, 7 Nov 2007 11:37:41 +0000 Subject: [PATCH 1/1] Add initial tags, storytags, and tagcloud plugins. --- MANIFEST.medium | 3 + gavinc/storytags | 124 +++++++++++++++++++++ gavinc/tagcloud | 160 +++++++++++++++++++++++++++ gavinc/tags | 279 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 566 insertions(+) create mode 100644 gavinc/storytags create mode 100644 gavinc/tagcloud create mode 100644 gavinc/tags diff --git a/MANIFEST.medium b/MANIFEST.medium index 548326b..e839daf 100644 --- a/MANIFEST.medium +++ b/MANIFEST.medium @@ -72,8 +72,11 @@ sort_by_path sort_reverse static_file storydate +storytags storytitle +tagcloud tagging +tags textile2 textile2.README theme diff --git a/gavinc/storytags b/gavinc/storytags new file mode 100644 index 0000000..148a48f --- /dev/null +++ b/gavinc/storytags @@ -0,0 +1,124 @@ +# Blosxom Plugin: storytags +# Author(s): Gavin Carr +# Version: 0.001000 +# Documentation: See the bottom of this file or type: perldoc storytags +# Requires: tags +# Follows: tags + +package storytags; + +use strict; + +# Uncomment next line to enable debug output (don't uncomment debug() lines) +#use Blosxom::Debug debug_level => 1; + +use vars qw($taglist); + +# --- Configuration variables ----- + +# Formatting strings +my $prefix = 'Tags: '; +my $suffix = '. '; + +# --------------------------------- + +$taglist = ''; + +sub start { 1 } + +# Set $taglist +sub story { + my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_; + + return 1 unless $tags::tag_cache + && ref $tags::tag_cache + && keys %{ $tags::tag_cache }; + return 1 unless defined $tags::tag_cache->{"$path/$filename"}; + + $taglist = _format_taglist($tags::tag_cache->{"$path/$filename"}->{tags}); + + return 1; +} + +sub _format_taglist { + my ($tags) = @_; + $tags = '' unless defined $tags; + my @tags = sort { lc $a cmp lc $b } split /\s*,\s*/, $tags; + return '' unless @tags; + return $prefix + . join(', ', + map { qq() } + @tags + ) + . $suffix; +} + +1; + +__END__ + +=head1 NAME + +storytags - blosxom plugin to format a per-story $storytags::taglist string + +=head1 DESCRIPTION + +L is a blosxom plugin to format a per-story $storytags::taglist +string. The string is a comma-separated list of the tags defined for the +story, prefixed by $storytags::prefix, and suffixed by $storytags::suffix. +If no tags are defined, then $taglist will be the empty string '' (i.e. no +prefix and suffix are added). + +The default values for $prefix and $suffix are 'Tags: ' and '. ' +respectively, so a typical $taglist might look like: + + Tags: dogs, cats, pets. + +=head1 USAGE + +L requires the L plugin, and should be loaded AFTER +L. It has no other ordering dependencies. + +=head1 ACKNOWLEDGEMENTS + +This plugin was inspired by xtaran's excellent L plugin, +which includes similar functionality. + +=head1 SEE ALSO + +L, L, xtaran's L. + +Blosxom: http://blosxom.sourceforge.net/ + +=head1 AUTHOR + +Gavin Carr , http://www.openfusion.net/ + +=head1 LICENSE + +Copyright 2007, Gavin Carr. + +This plugin is licensed under the same terms as blosxom itself i.e. + +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 + +# vim:ft=perl:sw=4 + diff --git a/gavinc/tagcloud b/gavinc/tagcloud new file mode 100644 index 0000000..c94ddb3 --- /dev/null +++ b/gavinc/tagcloud @@ -0,0 +1,160 @@ +# Blosxom Plugin: tagcloud +# Author(s): Gavin Carr +# Version: 0.002000 +# Documentation: See the bottom of this file or type: perldoc tagcloud + +package tagcloud; + +use strict; +use vars qw(%config $cloud); + +# Uncomment next line to enable debug output (don't uncomment debug() lines) +#use Blosxom::Debug debug_level => 1; + +# --- Configuration defaults ----- + +%config = ( + + # tagcloud requires a hashref containing 'tag => count' pairs + tag_hashref => $tags::tag_counts, + + # Formatting options + tagcloud_prefix => qq(

\n), + tagcloud_suffix => qq(

\n), + show_tag_no => 0, + min_tag_no => 2, + min_size => 75, + max_size => 200, + entry_type => 'posting', + + # Tags to omit from tagcloud + tagcloud_blacklist => [ 'Now Playing' ], + +); + +# --------------------------------- + +my %tagcloud_blacklist = map { $_ => 1 } @{$config{tagcloud_blacklist}}; + +$cloud = ''; + +sub start { + eval { $config{tag_hashref} } + or warn "[tagcloud] tag_hashref not set - skipping" + and return 0; + 1; +} + +sub story { + my $tag_hashref = $config{tag_hashref}; + return unless keys %$tag_hashref; + + my %tags = (); + my $min = undef; + my $max = 1; + for (keys %$tag_hashref) { + next if exists $tagcloud_blacklist{ $_ }; + next if $tag_hashref->{$_} < $config{min_tag_no}; + $tags{$_} = $tag_hashref->{$_}; + $min = $tag_hashref->{$_} if $min > $tag_hashref->{$_} || ! $min; + $max = $tag_hashref->{$_} if $max < $tag_hashref->{$_}; + } + return unless keys %tags; + + my $diff = $max - $min; + + my @tagcloud = (); + for my $tag (sort { lc $a cmp lc $b } keys %tags) { + my $label = $tag; + $label .= " ($tags{$tag})" if $config{show_tag_no}; + my $url_tag = _url_escape($tag); + my $url = "$blosxom::url/tags/$url_tag"; + my $plural = $tags{$tag} == 1 ? '' : 's'; + my $title = sprintf "%s %s%s tagged", $tags{$tag}, $config{entry_type}, $plural; + my $tag_percent = int($config{min_size} + + ((($config{max_size}-$config{min_size})/$diff) * ($tags{$tag}-$min+1))); + my $style = 'white-space: nowrap;'; + $style .= qq(font-size: $tag_percent%;") if $diff; + push @tagcloud, qq(); + } + + $cloud = sprintf "%s%s%s", + $config{tagcloud_prefix}, join(', ', @tagcloud), $config{tagcloud_suffix}; +} + +sub _url_escape { + my $s = shift; + $s =~ s/[^0-9A-Za-z,.:]/sprintf('%%%02X', ord($&))/seg; + return $s; +} + + +1; + +__END__ + +=head1 NAME + +tagcloud - blosxom plugin to set a $tagcloud::cloud template variable +given an arbitrary set of tags and counts + + +=head1 DESCRIPTION + +tagcloud is a blosxom plugin which sets a $tagcloud::cloud template +variable given an arbitrary set of tags and counts. It was developed +for use with the L plugin. + + +=head1 USAGE + +tagcloud should be loaded after the plugin that is generating its tag +set. + + +=head1 SEE ALSO + +L + +Blosxom: http://blosxom.sourceforge.net/ + + +=head1 ACKNOWLEDGEMENTS + +Portions of this plugin were swiped verbatim from xtaran's excellent +L plugin. + + +=head1 AUTHOR + +Gavin Carr , http://www.openfusion.net/ + + +=head1 LICENSE + +Copyright 2007, Gavin Carr. + +This plugin is licensed under the same terms as blosxom itself i.e. + +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 + +# vim:ft=perl:sw=4 + diff --git a/gavinc/tags b/gavinc/tags new file mode 100644 index 0000000..0202f85 --- /dev/null +++ b/gavinc/tags @@ -0,0 +1,279 @@ +# Blosxom Plugin: tags +# Author(s): Gavin Carr +# Version: 0.001000 +# Documentation: See the bottom of this file or type: perldoc tags + +package tags; + +use strict; +use File::stat; + +# Uncomment next line to enable debug output (don't uncomment debug() lines) +#use Blosxom::Debug debug_level => 2; + +use vars qw($tagroot $tag_cache $tag_entries $tag_counts); + +# --- Configuration variables ----- + +# What path prefix is used for tag-based queries? +$tagroot = "/tags"; + +# What story header to you use for your tags? +my $tag_header = 'Tags'; + +# Where is our $tag_cache file? +my $tag_cache_file = "$blosxom::plugin_state_dir/tag_cache"; + +# --------------------------------- + +$tag_cache = {}; +$tag_entries = {}; +$tag_counts = {}; +my $tag_cache_dirty = 0; + +my @path_tags; +my $path_tags_op; + +sub start { + # Load tag_cache + if (-f $tag_cache_file) { + my $fh = FileHandle->new( $tag_cache_file, 'r' ) + or warn "[tags] cannot open cache: $!"; + { + local $/ = undef; + eval <$fh>; + } + close $fh; + } + + 1; +} + +sub entries { + @path_tags = (); + my $path_info = "/$blosxom::path_info"; + # debug(3, "entries, path_info $path_info"); + + if ($path_info =~ m!^$tagroot/(.*)!) { + my $taglist = $1; + # debug(3, "entries, path_info matches tagroot (taglist $taglist)"); + if ($taglist =~ m/;/) { + @path_tags = split /\s*;\s*/, $taglist; + $path_tags_op = ';'; + } + else { + @path_tags = split /\s*,\s*/, $taglist; + $path_tags_op = ','; + } + # If $path_info matches $tagroot it's a virtual path, so reset + $blosxom::path_info = ''; + } + + return 0; +} + +sub filter { + my ($pkg, $files_ref) = @_; + return 1 unless @path_tags; + + my %tagged = (); + for my $tag (@path_tags) { + if ($tag_entries->{$tag} && @{ $tag_entries->{$tag} }) { + for my $entry ( @{ $tag_entries->{$tag} } ) { + $tagged{"$blosxom::datadir$entry.$blosxom::file_extension"}++; + } + } + } + # debug(2, "entries tagged with " . join($path_tags_op,@path_tags) . ":\n " . join("\n ", sort keys %tagged)); + + # Now delete all entries from $files_ref except those tagged + my $tag_count = scalar @path_tags; + for (keys %$files_ref) { + if ($path_tags_op eq ';') { + # OR semantics - delete unless at least one tag + delete $files_ref->{$_} unless exists $tagged{$_}; + } + else { + # AND semantics - delete unless ALL tags + delete $files_ref->{$_} unless $tagged{$_} == $tag_count; + } + } + + return 1; +} + +# Update tag cache on new or updated stories +sub story { + my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_; + + my $file = "$blosxom::datadir$path/$filename.$blosxom::file_extension"; + unless (-f $file) { + warn "[tags] cannot find story file '$file'"; + return 0; + } + + # Check story mtime + my $st = stat($file) or die "bad stat: $!"; + my $mtime = $st->mtime; + if ($tag_cache->{"$path/$filename"}->{mtime} == $mtime) { + # debug(3, "$path/$filename found up to date in tag_cache - skipping"); + return 1; + } + + # mtime has changed (or story is new) - compare old and new tagsets + my $tags_new = $blosxom::meta{$tag_header}; + my $tags_old = $tag_cache->{"$path/$filename"}->{tags}; + # debug(2, "tags_new: $tags_new, tags_old: $tags_old"); + + return 1 if defined $tags_new && defined $tags_old && $tags_old eq $tags_new; + + # Update tag_cache + # debug(2, "updating tag_cache, mtime $mtime, tags '$tags_new'"); + $tag_cache->{"$path/$filename"} = { mtime => $mtime, tags => $tags_new }; + $tag_cache_dirty++; +} + +# Write tag_cache to disk if updated +sub last { + if ($tag_cache_dirty) { + # Refresh tag entries and tag counts + $tag_entries = {}; + $tag_counts = {}; + for my $entry (keys %{$tag_cache}) { + next unless $tag_cache->{$entry}->{tags}; + for (split /\s*,\s*/, $tag_cache->{$entry}->{tags}) { + $tag_entries->{$_} ||= []; + push @{ $tag_entries->{$_} }, $entry; + $tag_counts->{$_}++; + } + } + + # Save tag caches back to $tag_cache_file + my $fh = FileHandle->new( $tag_cache_file, 'w' ) + or warn "[tags] cannot open cache '$tag_cache_file': $!" + and return 0; + print $fh Data::Dumper->Dump([ $tag_cache, $tag_entries, $tag_counts ], + [ qw(tag_cache tag_entries tag_counts) ]); + close $fh; + } +} + +1; + +__END__ + +=head1 NAME + +tags - blosxom plugin to read tags from story files, maintain a tag cache, +and allow tag-based filtering + +=head1 DESCRIPTION + +L is a blosxom plugin to read tags from story files, maintain a tag +cache, and allow tag-based filtering. + +Tags are defined in a comma-separated list in a $tag_header header at the +beginning of a blosxom post, with $tag_header defaulting to 'Tags'. So for +example, your post might look like: + + My Post Title + Tags: dogs, cats, pets + + Post text goes here ... + +L uses the L plugin to parse story headers, and stores +the tags in a cache (in $tag_cache_file, which defaults to +$blosxom::plugin_state_dir/tag_cache). The tag cache is only updated +when the mtime of the story file changes, so L should perform +pretty well. + +L also supports tag-based filtering. If the blosxom $path_info +begins with $tagroot ('/tags', by default, e.g. '/tags/dogs'), then +L filters the entries list to include only posts with the +specified tag. The following syntaxes are supported (assuming the +default '/tags' for $tagroot): + +=over 4 + +=item /tags/ e.g. /tags/dog + +Show only posts with the specified tag. + +=item /tags/,[,...] e.g. /tags/dogs,cats + +(Comma-separated) Show only posts with ALL of the specified tags i.e. +AND semantics. + +=item /tags/;[;...] e.g. /tags/dogs;cats + +(Comma-separated) Show only posts with ANY of the specified tags i.e. +OR semantics. + +=back + +=head1 USAGE + +L should be loaded early as it modifies blosxom $path_info when +doing tag-based filtering. Specifically, it needs to be loaded BEFORE +any C plugins (e.g. L, L, +L, etc.) + +L depends on the L plugin to parse the tags header, +though, so it must be loaded AFTER L. + +Also, because L does tag-based filtering, any filter plugins +that you want to have a global view of your entries (like +L, for example) should be loaded BEFORE L. + +=head1 ACKNOWLEDGEMENTS + +This plugin was inspired by xtaran's excellent L plugin. +Initially I was just looking to add caching to L, but found +I wanted to use a more modular approach, and wanted to do slightly +different filtering than L offered as well. L is +still more full-featured. + +=head1 SEE ALSO + +L only handles maintaining the tags cache and doing tag-based +filtering. For displaying tag lists in stories, see L. +For displaying a tagcloud, see L. + +L is used for the tags header parsing. + +See also xtaran's L plugin, which inspired L. + +Blosxom: http://blosxom.sourceforge.net/ + +=head1 AUTHOR + +Gavin Carr , http://www.openfusion.net/ + +=head1 LICENSE + +Copyright 2007, Gavin Carr. + +This plugin is licensed under the same terms as blosxom itself i.e. + +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 + +# vim:ft=perl:sw=4 + -- 2.30.2