root/tagwire/tags/0.20/tagwire.pl

Revision 95, 15.6 kB (checked in by ogawa, 3 years ago)

Add README.txt.
Code cleanup and a bug fix.

  • Property svn:keywords set to Id Author Date Rev
Line 
1# Tagwire Plugin (aka AllKeywords Plugin)
2# a plugin for listing and handling "tags"
3#
4# $Id$
5#
6# This software is provided as-is. You may use it for commercial or
7# personal use. If you distribute it, please keep this notice intact.
8#
9# Copyright (c) 2005 Hirotaka Ogawa
10
11package MT::Plugin::Tagwire;
12use strict;
13use MT::Template::Context;
14use MT::Request;
15
16# DEBUG
17my $FORCE_PD_REFRESH = 0;
18my $ENABLE_PD_INDEXES = 1;
19my $ENABLE_REQ_CACHE = 1;
20
21my $plugin;
22eval {
23    require MT::Plugin;
24    $plugin = new MT::Plugin();
25    $plugin->name("Tagwire Plugin");
26    $plugin->description("A plugin for listing and handling blog-wide tags and entry tags.");
27    $plugin->doc_link("http://as-is.net/hacks/2005/06/tagwire_plugin.html");
28    MT->add_plugin($plugin);
29};
30
31if (MT->can('add_callback')) {
32    my $mt = MT->instance;
33    MT->add_callback((ref $mt eq 'MT::App::CMS' ? 'AppPostEntrySave' : 'MT::Entry::post_save'),
34                     10, $plugin, \&update_pd_indexes);
35}
36
37sub update_pd_indexes {
38    return unless $ENABLE_PD_INDEXES && $plugin;
39    my ($eh, $app, $entry) = @_;
40    require MT::Entry;
41    my $blog_id = $entry->blog_id;
42    require MT::PluginData;
43    my $pd = MT::PluginData->load({ plugin => $plugin->name,
44                                    key => $blog_id });
45    my (%eindex, %tindex);
46    my $data;
47    if (!$pd || $FORCE_PD_REFRESH) {
48        $pd = new MT::PluginData();
49        $pd->plugin($plugin->name);
50        $pd->key($blog_id);
51        $data = $pd->data() || {};
52        my $iter = MT::Entry->load_iter({ blog_id => $blog_id,
53                                          status => MT::Entry::RELEASE() });
54        while (my $e = $iter->()) {
55            my @tags = split_tags($e->keywords, 1) or next;
56            $eindex{$e->id} = { tags => \@tags,
57                                created_on => $e->created_on };
58        }
59    } else {
60        $data = $pd->data() || {};
61        my $entry_id = $entry->id;
62        %eindex = %{$data->{eindex}};
63        delete $eindex{$entry_id} if exists $eindex{$entry_id};
64        if ($entry->status == MT::Entry::RELEASE()) {
65            my @tags = split_tags($entry->keywords, 1);
66            $eindex{$entry_id} = { tags => \@tags,
67                                   created_on => $entry->created_on };
68        }
69    }
70    foreach my $eid (keys %eindex) {
71        map { push @{$tindex{$_}}, $eid } @{$eindex{$eid}->{tags}};
72    }
73    $data->{eindex} = \%eindex;
74    $data->{tindex} = \%tindex;
75    $pd->data($data);
76    $pd->save or die $pd->errstr;
77    if ($ENABLE_REQ_CACHE) {
78        my $r = MT::Request->instance;
79        my $cname = 'Tagwire::Cache::' . $blog_id;
80        $r->cache($cname, undef);
81    }
82}
83
84MT::Template::Context->add_container_tag('Tags' => \&tags);
85MT::Template::Context->add_container_tag('EntryTags' => \&entry_tags);
86MT::Template::Context->add_tag('Tag' => \&tag);
87MT::Template::Context->add_tag('TagCount' => \&tag_count);
88MT::Template::Context->add_tag('TagsTotal' => \&tags_total);
89MT::Template::Context->add_tag('TagsTotalSum' => \&tags_total_sum);
90MT::Template::Context->add_container_tag('EntriesWithTags' => \&entries);
91MT::Template::Context->add_container_tag('MostRelatedEntries' => \&most_related_entries);
92
93# For compatibility (this plugin was formerly named 'AllKeywords')
94MT::Template::Context->add_container_tag('AllKeywords' => \&tags);
95MT::Template::Context->add_container_tag('EntryAllKeywords' => \&entry_tags);
96MT::Template::Context->add_tag('AllKeyword' => \&tag);
97MT::Template::Context->add_tag('AllKeywordCount' => \&tag_count);
98MT::Template::Context->add_tag('AllKeywordsTotal' => \&tags_total);
99MT::Template::Context->add_tag('AllKeywordsTotalSum' => \&tags_total_sum);
100MT::Template::Context->add_container_tag('EntriesWithKeywords' => \&entries);
101
102sub split_args {
103    my ($string, $delimiter, $case_sensitive) = @_;
104    return unless $string;
105    my @tags;
106    $string =~ s/(^\s+|\s+$)//g;
107    $string = lc $string unless $case_sensitive;
108
109    return split(/\s+/, $string) unless $delimiter;
110
111    foreach my $tag (split($delimiter, $string)) {
112        $tag =~ s/(^\s+|\s+$)//g;
113        push @tags, $tag if $tag;
114    }
115    @tags;
116}
117
118sub split_tags {
119    my ($string, $case_sensitive) = @_;
120    return unless $string;
121    my @tags;
122    $string =~ s/(^\s+|\s+$)//g;
123    $string = lc $string unless $case_sensitive;
124
125    if ($string =~ m/[;,|]/) {
126        # tags separated by non-whitespaces
127        while ($string =~ m/(\[[^]]+\]|"[^"]+"|'[^']+'|[^;,|]+)/g) {
128            my $tag = $1;
129            $tag =~ s/(^[\["'\s;,|]+|[\]"'\s;,|]+$)//g;
130            push @tags, $tag if $tag;
131        }
132    } else {
133        # tags separated by whitespaces
134        while ($string =~ m/(\[[^]]+\]|"[^"]+"|'[^']+'|[^\s]+)/g) {
135            my $tag = $1;
136            $tag =~ s/(^[\["'\s]+|[\]"'\s]+$)//g;
137            push @tags, $tag if $tag;
138        }
139    }
140    @tags;
141}
142
143sub get_pd_indexes {
144    return unless $ENABLE_PD_INDEXES && $plugin;
145    my $blog_id = $_[0] or return;
146    my ($r, $cname);
147    if ($ENABLE_REQ_CACHE) {
148        $r = MT::Request->instance;
149        $cname = 'Tagwire::Cache::' . $blog_id;
150        return $r->cache($cname) if defined $r->cache($cname);
151        $r->cache($cname, undef);
152    }
153    my $data;
154    eval {
155        require MT::PluginData;
156        my $pd = MT::PluginData->load({ plugin => $plugin->name,
157                                        key => $blog_id });
158        $data = $pd->data() if $pd;
159    };
160    $r->cache($cname, $data) if $ENABLE_REQ_CACHE && $data;
161    $data;
162}
163
164sub get_db_indexes {
165    my $blog_id = $_[0] or return;
166    my ($r, $cname);
167    if ($ENABLE_REQ_CACHE) {
168        $r = MT::Request->instance;
169        $cname = 'Tagwire::Cache::' . $blog_id;
170        return $r->cache($cname) if defined $r->cache($cname);
171        $r->cache($cname, undef);
172    }
173    my $data;
174    my (%eindex, %tindex);
175    my $iter = MT::Entry->load_iter({ blog_id => $blog_id,
176                                      status => MT::Entry::RELEASE() });
177    while (my $e = $iter->()) {
178        my @tags = split_tags($e->keywords, 1) or next;
179        $eindex{$e->id} = { tags => \@tags,
180                            created_on => $e->created_on };
181    }
182    foreach my $eid (keys %eindex) {
183        map { push @{$tindex{$_}}, $eid } @{$eindex{$eid}->{tags}};
184    }
185    $data->{eindex} = \%eindex;
186    $data->{tindex} = \%tindex;
187    $r->cache($cname, $data) if $ENABLE_REQ_CACHE;
188    $data;
189}
190
191sub tags {
192    my ($ctx, $args, $cond) = @_;
193
194    # sort_by (tag/tag-case/count, default = tag)
195    my $sort_by = $args->{sort_by} || 'tag';
196    # sort_order (ascend/descend, default = ascend)
197    my $sort_order = $args->{sort_order} || 'ascend';
198    # lastn (default = 0, no cutoff)
199    my $lastn = $args->{lastn} || 0;
200    # case_sensitive (0/1, default = 1)
201    my $case_sensitive = defined $args->{case_sensitive} ?
202        $args->{case_sensitive} : 1;
203
204    my $blog_id = $ctx->stash('blog_id');
205    my %tags = ();
206
207    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
208        or return '';
209    my %tindex = %{$data->{tindex}};
210    if ($case_sensitive) {
211        map { $tags{$_} = scalar(@{$tindex{$_}}) } keys %tindex;
212    } else {
213        map { $tags{lc $_} += scalar(@{$tindex{$_}}) } keys %tindex;
214    }
215
216    my @list;
217    if ($sort_by eq 'tag' || $sort_by eq 'keyword' ) {
218        @list = $sort_order eq 'ascend' ?
219            sort { lc $a cmp lc $b } keys %tags :
220            sort { lc $b cmp lc $a } keys %tags;
221    } elsif ($sort_by eq 'tag-case' || $sort_by eq 'keyword-case') {
222        @list = $sort_order eq 'ascend' ?
223            sort keys %tags :
224            sort reverse keys %tags;
225    } else {
226        @list = $sort_order eq 'ascend' ?
227            sort { $tags{$a} <=> $tags{$b} } keys %tags :
228            sort { $tags{$b} <=> $tags{$a} } keys %tags;
229    }
230
231    $ctx->stash('Tagwire::tags_total', scalar @list);
232
233    my $total_sum = 0;
234    foreach (@list) {
235        $total_sum += $tags{$_};
236    }
237    $ctx->stash('Tagwire::tags_total_sum', $total_sum);
238
239    my @res;
240    my $builder = $ctx->stash('builder');
241    my $tokens = $ctx->stash('tokens');
242    my $i = 0;
243    foreach (@list) {
244        last if $lastn && $i >= $lastn;
245        $ctx->stash('Tagwire::tag', $case_sensitive ? $_ : ucfirst $_);
246        $ctx->stash('Tagwire::tag_count', $tags{$_});
247        defined(my $out = $builder->build($ctx, $tokens))
248            or return $ctx->error($ctx->errstr);
249        push @res, $out;
250        $i++;
251    }
252    my $glue = $args->{glue} || '';
253    join $glue, @res;
254}
255
256sub entry_tags {
257    my ($ctx, $args, $cond) = @_;
258    my $e = $ctx->stash('entry')
259        or return $ctx->_no_entry_error('MT' . $ctx->stash('tag'));
260    return '' unless $e->keywords;
261
262    # case_sensitive (0/1, default = 1)
263    my $case_sensitive = defined $args->{case_sensitive} ?
264        $args->{case_sensitive} : 1;
265
266    my @tags = split_tags($e->keywords, $case_sensitive);
267    my $total = scalar(@tags);
268    $ctx->stash('Tagwire::tags_total', $total);
269    $ctx->stash('Tagwire::tags_total_sum', $total);
270
271    my @res;
272    my $builder = $ctx->stash('builder');
273    my $tokens = $ctx->stash('tokens');
274    foreach (@tags) {
275        $ctx->stash('Tagwire::tag', $case_sensitive ? $_ : ucfirst $_);
276        $ctx->stash('Tagwire::tag_count', 1);
277        defined(my $out = $builder->build($ctx, $tokens))
278            or return $ctx->error($ctx->errstr);
279        push @res, $out;
280    }
281    my $glue = $args->{glue} || '';
282    join $glue, @res;
283}
284
285sub tag {
286    $_[0]->stash('Tagwire::tag');
287}
288
289sub tag_count {
290    $_[0]->stash('Tagwire::tag_count');
291}
292
293sub tags_total {
294    $_[0]->stash('Tagwire::tags_total');
295}
296
297sub tags_total_sum {
298    $_[0]->stash('Tagwire::tags_total_sum');
299}
300
301sub entries {
302    my ($ctx, $args, $cond) = @_;
303
304    # tags(keywords) (REQUIRED)
305    my $search = $args->{tags} || $args->{keywords} or return '';
306    # delimiter for "tags" argument (default = space characters)
307    my $delimiter = $args->{delimiter} || '';
308    # case_sensitive (0/1, default = 1)
309    my $case_sensitive = defined $args->{case_sensitive} ?
310        $args->{case_sensitive} : 1;
311    # sort_by (created_on, default = created_on)
312    my $sort_by = $args->{sort_by} || 'created_on';
313    # sort_order (ascend/descend, default = descend)
314    my $sort_order = $args->{sort_order} || 'descend';
315    # lastn (default = 0, no cutoff)
316    my $lastn = $args->{lastn} || 0;
317
318    my @patterns = split_args($search, $delimiter, $case_sensitive)
319        or return '';
320
321    my $blog_id = $ctx->stash('blog_id');
322    my @entries;
323
324    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
325        or return '';
326    my %tindex = %{$data->{tindex}};
327    my %eindex = %{$data->{eindex}};
328    my %match;
329    if ($case_sensitive) {
330        foreach my $tag (@patterns) {
331            foreach my $eid (@{$tindex{$tag}}) {
332                $match{$eid} = exists $match{$eid} ? $match{$eid} + 1 : 1;
333            }
334        }
335    } else {
336        foreach my $tag (@patterns) {
337            foreach my $mtag (grep { lc $_ eq $tag } keys %tindex) {
338                foreach my $eid (@{$tindex{$mtag}}) {
339                    $match{$eid} = exists $match{$eid} ? $match{$eid} + 1 : 1;
340                }
341            }
342        }
343    }
344    my $count = scalar @patterns;
345    my @eids = grep { $match{$_} == $count } keys %match or return;
346    @eids = sort { $eindex{$b}->{created_on} <=> $eindex{$a}->{created_on} } @eids;
347    @eids = $sort_order eq 'descend' ?
348        sort { $eindex{$b}->{created_on} <=> $eindex{$a}->{created_on} } @eids :
349        sort { $eindex{$a}->{created_on} <=> $eindex{$b}->{created_on} } @eids;
350    splice(@eids, $lastn) if $lastn && (scalar @eids > $lastn);
351    require MT::Entry;
352    map { push @entries, MT::Entry->load($_) } @eids;
353
354    my $res = '';
355    my $tokens = $ctx->stash('tokens');
356    my $builder = $ctx->stash('builder');
357    my $i = 0;
358    for my $e (@entries) {
359        local $ctx->{__stash}{entry} = $e;
360        local $ctx->{current_timestamp} = $e->created_on;
361        local $ctx->{modification_timestamp} = $e->modified_on;
362        my $out = $builder->build($ctx, $tokens, {
363            %$cond,
364            EntryIfExtended => $e->text_more ? 1 : 0,
365            EntryIfAllowComments => $e->allow_comments,
366            EntryIfCommentsOpen => $e->allow_comments && $e->allow_comments eq '1',
367            EntryIfAllowPings => $e->allow_pings,
368            EntriesHeader => !$i,
369            EntriesFooter => !defined $entries[$i+1]
370            });
371        return $ctx->error($ctx->errstr) unless defined $out;
372        $res .= $out;
373        $i++;
374    }
375    $res;
376}
377
378sub most_related_entries {
379    my ($ctx, $args, $cond) = @_;
380    my $entry = $ctx->stash('entry')
381        or return $ctx->_no_entry_error('MT' . $ctx->stash('tag'));
382    return '' unless $entry->keywords;
383
384    # case_sensitive (0/1, default = 1)
385    my $case_sensitive = defined $args->{case_sensitive} ?
386        $args->{case_sensitive} : 1;
387    # sort_order (ascend/descend, default = descend)
388    my $sort_order = $args->{sort_order} || 'descend';
389    # lastn (default = 0, no cutoff)
390    my $lastn = $args->{lastn} || 0;
391
392    my @patterns = split_tags($entry->keywords, $case_sensitive)
393        or return '';
394
395    my $blog_id = $ctx->stash('blog_id');
396    my @entries;
397
398    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
399        or return '';
400    my %tindex = %{$data->{tindex}};
401    my %eindex = %{$data->{eindex}};
402    my %match;
403    if ($case_sensitive) {
404        foreach my $tag (@patterns) {
405            foreach my $eid (@{$tindex{$tag}}) {
406                next if $eid == $entry->id;
407                $match{$eid} = exists $match{$eid} ? $match{$eid} + 1 : 1;
408            }
409        }
410    } else {
411        foreach my $tag (@patterns) {
412            foreach my $mtag (grep { lc $_ eq $tag } keys %tindex) {
413                foreach my $eid (@{$tindex{$mtag}}) {
414                    next if $eid == $entry->id;
415                    $match{$eid} = exists $match{$eid} ? $match{$eid} + 1 : 1;
416                }
417            }
418        }
419    }
420    my @eids = keys %match or return '';
421    @eids = $sort_order eq 'descend' ?
422        sort { $eindex{$b}->{created_on} <=> $eindex{$a}->{created_on} } @eids :
423        sort { $eindex{$a}->{created_on} <=> $eindex{$b}->{created_on} } @eids;
424    @eids = sort { $match{$b} <=> $match{$a} } @eids;
425    splice(@eids, $lastn) if $lastn && (scalar @eids > $lastn);
426    require MT::Entry;
427    map { push @entries, MT::Entry->load($_) } @eids;
428    return '' unless @entries;
429
430    my $res = '';
431    my $tokens = $ctx->stash('tokens');
432    my $builder = $ctx->stash('builder');
433    my $i = 0;
434    for my $e (@entries) {
435        local $ctx->{__stash}{entry} = $e;
436        local $ctx->{current_timestamp} = $e->created_on;
437        local $ctx->{modification_timestamp} = $e->modified_on;
438        my $out = $builder->build($ctx, $tokens, {
439            %$cond,
440            EntryIfExtended => $e->text_more ? 1 : 0,
441            EntryIfAllowComments => $e->allow_comments,
442            EntryIfCommentsOpen => $e->allow_comments && $e->allow_comments eq '1',
443            EntryIfAllowPings => $e->allow_pings,
444            EntriesHeader => !$i,
445            EntriesFooter => !defined $entries[$i+1]
446            });
447        return $ctx->error($ctx->errstr) unless defined $out;
448        $res .= $out;
449        $i++;
450    }
451    $res;
452}
453
454eval {
455    require MT::XSearch;
456    MT::XSearch->add_search_plugin('Tagwire', {
457        label => 'Tag(Keyword) Search',
458        description => 'Tag(Keyword) Search plugin for MT-XSearch',
459        on_execute => \&xsearch_on_execute,
460        on_stash => \&xsearch_on_stash });
461};
462
463sub xsearch_on_stash {
464    $_[0]->stash('entry', $_[1]);
465    $_[0]->{current_timestamp} = $_[1]->created_on;
466    $_[0]->{modification_timestamp} = $_[1]->modified_on;
467}
468
469sub xsearch_on_execute {
470    my $args = shift;
471    require MT::Entry;
472    my $blog_id = $args->{blog_id} or MT->error('Blog ID is required.');
473    my $sort_by = $args->{sort_by} || 'created_on';
474    my $sort_order = $args->{sort_order} || 'descend';
475    my $delimiter = $args->{delimiter} || '';
476    my $case_sensitive = defined $args->{case_sensitive} ?
477        $args->{case_sensitive} : 1;
478
479    my @results;
480    my @patterns = split_args($args->{search}, $delimiter, $case_sensitive);
481
482    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
483        or return \@results;
484    my %tindex = %{$data->{tindex}};
485    my %eindex = %{$data->{eindex}};
486    my %match;
487    if ($case_sensitive) {
488        foreach my $tag (@patterns) {
489            foreach my $eid (@{$tindex{$tag}}) {
490                $match{$eid} = exists $match{$eid} ? $match{$eid} + 1 : 1;
491            }
492        }
493    } else {
494        foreach my $tag (@patterns) {
495            foreach my $mtag (grep { lc $_ eq $tag } keys %tindex) {
496                foreach my $eid (@{$tindex{$mtag}}) {
497                    $match{$eid} = exists $match{$eid} ? $match{$eid} + 1 : 1;
498                }
499            }
500        }
501    }
502    my $count = scalar @patterns;
503    my @eids = grep { $match{$_} == $count } keys %match
504        or return \@results;
505    @eids = $sort_order eq 'descend' ?
506        sort { $eindex{$b}->{created_on} <=> $eindex{$a}->{created_on} } @eids :
507        sort { $eindex{$a}->{created_on} <=> $eindex{$b}->{created_on} } @eids;
508    require MT::Entry;
509    map { push @results, MT::Entry->load($_) } @eids;
510    \@results;
511}
512
5131;
Note: See TracBrowser for help on using the browser.