Add initial tags, storytags, and tagcloud plugins.
authorGavin Carr <gonzai@users.sourceforge.net>
Wed, 7 Nov 2007 11:37:41 +0000 (11:37 +0000)
committerGavin Carr <gonzai@users.sourceforge.net>
Wed, 7 Nov 2007 11:37:41 +0000 (11:37 +0000)
MANIFEST.medium
gavinc/storytags [new file with mode: 0644]
gavinc/tagcloud [new file with mode: 0644]
gavinc/tags [new file with mode: 0644]

index 548326b1849d67eb799f81daa8d45ec135682044..e839daf07b76f4aca5ac8a8ab56cefeabdabcd5e 100644 (file)
@@ -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 (file)
index 0000000..148a48f
--- /dev/null
@@ -0,0 +1,124 @@
+# Blosxom Plugin: storytags
+# Author(s): Gavin Carr <gavin@openfusion.com.au>
+# 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(<a href="$blosxom::url/tags/$_" rel="tag">$_</a>) }
+               @tags
+           )
+           . $suffix;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+storytags - blosxom plugin to format a per-story $storytags::taglist string
+
+=head1 DESCRIPTION
+
+L<storytags> 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<storytags> requires the L<tags> plugin, and should be loaded AFTER
+L<tags>. It has no other ordering dependencies.
+
+=head1 ACKNOWLEDGEMENTS
+
+This plugin was inspired by xtaran's excellent L<tagging> plugin,
+which includes similar functionality.
+
+=head1 SEE ALSO
+
+L<tags>, L<tagcloud>, xtaran's L<tagging>.
+
+Blosxom: http://blosxom.sourceforge.net/
+
+=head1 AUTHOR
+
+Gavin Carr <gavin@openfusion.com.au>, 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 (file)
index 0000000..c94ddb3
--- /dev/null
@@ -0,0 +1,160 @@
+# Blosxom Plugin: tagcloud
+# Author(s): Gavin Carr <gavin@openfusion.com.au>
+# 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(<p class="tagcloud">\n),
+  tagcloud_suffix       => qq(</p>\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(<a href="$url" rel="tag" title="$title" style="$style">$label</a>);
+    }
+
+    $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<tags> plugin.
+
+
+=head1 USAGE
+
+tagcloud should be loaded after the plugin that is generating its tag
+set.
+
+
+=head1 SEE ALSO
+
+L<tags>
+
+Blosxom: http://blosxom.sourceforge.net/
+
+
+=head1 ACKNOWLEDGEMENTS
+
+Portions of this plugin were swiped verbatim from xtaran's excellent 
+L<tagging> plugin.
+
+
+=head1 AUTHOR
+
+Gavin Carr <gavin@openfusion.com.au>, 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 (file)
index 0000000..0202f85
--- /dev/null
@@ -0,0 +1,279 @@
+# Blosxom Plugin: tags
+# Author(s): Gavin Carr <gavin@openfusion.com.au>
+# 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<tags> 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<tags> uses the L<metamail> 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<tags> should perform
+pretty well.
+
+L<tags> also supports tag-based filtering. If the blosxom $path_info 
+begins with $tagroot ('/tags', by default, e.g. '/tags/dogs'), then 
+L<tags> 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/<tag> e.g. /tags/dog
+
+Show only posts with the specified tag.
+
+=item /tags/<tag1>,<tag2>[,<tag3>...] e.g. /tags/dogs,cats
+
+(Comma-separated) Show only posts with ALL of the specified tags i.e.
+AND semantics.
+
+=item /tags/<tag1>;<tag2>[;<tag3>...] e.g. /tags/dogs;cats
+
+(Comma-separated) Show only posts with ANY of the specified tags i.e.
+OR semantics.
+
+=back
+
+=head1 USAGE
+
+L<tags> should be loaded early as it modifies blosxom $path_info when 
+doing tag-based filtering. Specifically, it needs to be loaded BEFORE
+any C<entries> plugins (e.g. L<entries_index>, L<entries_cache>, 
+L<entries_timestamp>, etc.)
+
+L<tags> depends on the L<metamail> plugin to parse the tags header, 
+though, so it must be loaded AFTER L<metamail>.
+
+Also, because L<tags> does tag-based filtering, any filter plugins 
+that you want to have a global view of your entries (like 
+L<recententries>, for example) should be loaded BEFORE L<tags>. 
+
+=head1 ACKNOWLEDGEMENTS
+
+This plugin was inspired by xtaran's excellent L<tagging> plugin.
+Initially I was just looking to add caching to L<tagging>, but found
+I wanted to use a more modular approach, and wanted to do slightly 
+different filtering than L<tagging> offered as well. L<tagging> is
+still more full-featured.
+
+=head1 SEE ALSO
+
+L<tags> only handles maintaining the tags cache and doing tag-based
+filtering. For displaying tag lists in stories, see L<storytags>.
+For displaying a tagcloud, see L<tagcloud>.
+
+L<metamail> is used for the tags header parsing.
+
+See also xtaran's L<tagging> plugin, which inspired L<tags>.
+
+Blosxom: http://blosxom.sourceforge.net/
+
+=head1 AUTHOR
+
+Gavin Carr <gavin@openfusion.com.au>, 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
+