2 # Author(s): Gavin Carr <gavin@openfusion.com.au>
4 # Documentation: See the bottom of this file or type: perldoc tags
11 # Uncomment next line to enable debug output (don't uncomment debug() lines)
12 #use Blosxom::Debug debug_level => 2;
14 use vars qw(%config $tag_cache $tag_entries $tag_counts);
16 # --- Configuration variables -----
20 # What path prefix is used for tag-based queries?
21 $config{tagroot} = "/tags";
23 # What story header to you use for your tags?
24 $config{tag_header} = 'Tags';
26 # Where is our $tag_cache file?
27 $config{tag_cache_file} = "$blosxom::plugin_state_dir/tag_cache";
29 # ---------------------------------
35 my $tag_cache_dirty = 0;
42 if (-f $config{tag_cache_file}) {
43 my $fh = FileHandle->new( $config{tag_cache_file}, 'r' )
44 or warn "[tags] cannot open cache: $!";
57 my $path_info = "/$blosxom::path_info";
58 # debug(3, "entries, path_info $path_info");
60 if ($path_info =~ m!^$config{tagroot}/(.*)!) {
61 $blosxom::flavour = '';
63 # debug(3, "entries, path_info matches tagroot (taglist $taglist)");
65 # Allow flavours appended to tags after a slash
66 # Dot-flavour versions are problematic, because tags can include dot e.g. web2.0
67 if ($taglist =~ m! /(\w+)$ !x) {
68 $blosxom::flavour = $1;
69 $taglist =~ s! /$blosxom::flavour$ !!x;
72 # Split individual tags out of taglist
73 if ($taglist =~ m/;/) {
74 @path_tags = split /\s*;\s*/, $taglist;
78 @path_tags = split /\s*,\s*/, $taglist;
81 # If $path_info matches tagroot it's a virtual path, so reset
82 $blosxom::path_info = '';
83 $blosxom::flavour ||= $blosxom::default_flavour;
90 my ($pkg, $files_ref) = @_;
91 return 1 unless @path_tags;
94 for my $tag (@path_tags) {
95 if ($tag_entries->{$tag} && @{ $tag_entries->{$tag} }) {
96 for my $entry ( @{ $tag_entries->{$tag} } ) {
97 $tagged{"$blosxom::datadir$entry.$blosxom::file_extension"}++;
101 # debug(2, "entries tagged with " . join($path_tags_op,@path_tags) . ":\n " . join("\n ", sort keys %tagged));
103 # Now delete all entries from $files_ref except those tagged
104 my $tag_count = scalar @path_tags;
105 for (keys %$files_ref) {
106 if ($path_tags_op eq ';') {
107 # OR semantics - delete unless at least one tag
108 delete $files_ref->{$_} unless exists $tagged{$_};
111 # AND semantics - delete unless ALL tags
112 delete $files_ref->{$_} unless $tagged{$_} == $tag_count;
119 # Update tag cache on new or updated stories
121 my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
123 my $file = "$blosxom::datadir$path/$filename.$blosxom::file_extension";
125 warn "[tags] cannot find story file '$file'";
130 my $st = stat($file) or die "bad stat: $!";
131 my $mtime = $st->mtime;
132 if ($tag_cache->{"$path/$filename"}->{mtime} == $mtime) {
133 # debug(3, "$path/$filename found up to date in tag_cache - skipping");
137 # mtime has changed (or story is new) - compare old and new tagsets
138 my $tags_new = $blosxom::meta{$config{tag_header}};
139 my $tags_old = $tag_cache->{"$path/$filename"}->{tags};
140 # debug(2, "tags_new: $tags_new, tags_old: $tags_old");
142 return 1 if defined $tags_new && defined $tags_old && $tags_old eq $tags_new;
145 # debug(2, "updating tag_cache, mtime $mtime, tags '$tags_new'");
146 $tag_cache->{"$path/$filename"} = { mtime => $mtime, tags => $tags_new };
150 # Write tag_cache to disk if updated
152 if ($tag_cache_dirty) {
153 # Refresh tag entries and tag counts
156 for my $entry (keys %{$tag_cache}) {
157 next unless $tag_cache->{$entry}->{tags};
158 for (split /\s*,\s*/, $tag_cache->{$entry}->{tags}) {
159 $tag_entries->{$_} ||= [];
160 push @{ $tag_entries->{$_} }, $entry;
165 # Save tag caches back to $config{tag_cache_file}
166 my $fh = FileHandle->new( $config{tag_cache_file}, 'w' )
167 or warn "[tags] cannot open cache '$config{tag_cache_file}': $!"
169 print $fh Data::Dumper->Dump([ $tag_cache, $tag_entries, $tag_counts ],
170 [ qw(tag_cache tag_entries tag_counts) ]);
181 tags - blosxom plugin to read tags from story files, maintain a tag cache,
182 and allow tag-based filtering
186 L<tags> is a blosxom plugin to read tags from story files, maintain a tag
187 cache, and allow tag-based filtering.
189 Tags are defined in a comma-separated list in a $config{tag_header} header
190 at the beginning of a blosxom post, with $tag_header defaulting to 'Tags'.
191 So for example, your post might look like:
194 Tags: dogs, cats, pets
196 Post text goes here ...
198 L<tags> uses the L<metamail> plugin to parse story headers, and stores
199 the tags in a cache (in $config{tag_cache_file}, which defaults to
200 $blosxom::plugin_state_dir/tag_cache). The tag cache is only updated
201 when the mtime of the story file changes, so L<tags> should perform
204 L<tags> also supports tag-based filtering. If the blosxom $path_info
205 begins with $config{tagroot} ('/tags', by default, e.g. '/tags/dogs'),
206 then L<tags> filters the entries list to include only posts with the
207 specified tag. The following syntaxes are supported (assuming the
208 default '/tags' for 'tagroot'):
212 =item /tags/<tag> e.g. /tags/dog
214 Show only posts with the specified tag.
216 =item /tags/<tag1>,<tag2>[,<tag3>...] e.g. /tags/dogs,cats
218 (Comma-separated) Show only posts with ALL of the specified tags i.e.
221 =item /tags/<tag1>;<tag2>[;<tag3>...] e.g. /tags/dogs;cats
223 (Comma-separated) Show only posts with ANY of the specified tags i.e.
228 Tag filtering also supports a trailing flavour after the taglist,
229 separated by a slash e.g.
233 /tags/dogs;cats;pets/atom
235 Note that this is different to L<tagging>, which treats trailing
236 components as additional tags.
238 At this point L<tags> don't support dot-flavour paths e.g.
242 The problem with this is that tags can include dots, so it's
243 ambiguous how to parse C</tags/web2.0>, for instance. If you'd
244 like this supported and have suggestions about how to handle
245 the ambiguities, please get in touch.
249 L<tags> should be loaded early as it modifies blosxom $path_info when
250 doing tag-based filtering. Specifically, it needs to be loaded BEFORE
251 any C<entries> plugins (e.g. L<entries_index>, L<entries_cache>,
252 L<entries_timestamp>, etc.)
254 L<tags> depends on the L<metamail> plugin to parse the tags header,
255 though, so it must be loaded AFTER L<metamail>.
257 Also, because L<tags> does tag-based filtering, any filter plugins
258 that you want to have a global view of your entries (like
259 L<recententries>, for example) should be loaded BEFORE L<tags>.
261 =head1 ACKNOWLEDGEMENTS
263 This plugin was inspired by xtaran's excellent L<tagging> plugin.
264 Initially I was just looking to add caching to L<tagging>, but found
265 I wanted to use a more modular approach, and wanted to do slightly
266 different filtering than L<tagging> offered as well. L<tagging> is
267 still more full-featured.
271 L<tags> only handles maintaining the tags cache and doing tag-based
272 filtering. For displaying tag lists in stories, see L<storytags>.
273 For displaying a tagcloud, see L<tagcloud>.
275 L<metamail> is used for the tags header parsing.
277 See also xtaran's L<tagging> plugin, which inspired L<tags>.
279 Blosxom: http://blosxom.sourceforge.net/
283 Gavin Carr <gavin@openfusion.com.au>, http://www.openfusion.net/
287 Copyright 2007, Gavin Carr.
289 This plugin is licensed under the same terms as blosxom itself i.e.
291 Permission is hereby granted, free of charge, to any person obtaining a
292 copy of this software and associated documentation files (the "Software"),
293 to deal in the Software without restriction, including without limitation
294 the rights to use, copy, modify, merge, publish, distribute, sublicense,
295 and/or sell copies of the Software, and to permit persons to whom the
296 Software is furnished to do so, subject to the following conditions:
298 The above copyright notice and this permission notice shall be included
299 in all copies or substantial portions of the Software.
301 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
302 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
303 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
304 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
305 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
306 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
307 OTHER DEALINGS IN THE SOFTWARE.