]> sipb.mit.edu Git - ikiwiki.git/blob - IkiWiki/Plugin/trail.pm
trail: new plugin (3rd attempt)
[ikiwiki.git] / IkiWiki / Plugin / trail.pm
1 #!/usr/bin/perl
2 # Copyright © 2008-2011 Joey Hess
3 # Copyright © 2009-2011 Simon McVittie <http://smcv.pseudorandom.co.uk/>
4 # Licensed under the GNU GPL, version 2, or any later version published by the
5 # Free Software Foundation
6 package IkiWiki::Plugin::trail;
7
8 use warnings;
9 use strict;
10 use IkiWiki 3.00;
11
12 sub import {
13         hook(type => "getsetup", id => "trail", call => \&getsetup);
14         hook(type => "needsbuild", id => "trail", call => \&needsbuild);
15         hook(type => "preprocess", id => "trail", call => \&preprocess_trail, scan => 1);
16         hook(type => "preprocess", id => "trailinline", call => \&preprocess_trailinline, scan => 1);
17         hook(type => "preprocess", id => "trailitem", call => \&preprocess_trailitem, scan => 1);
18         hook(type => "preprocess", id => "traillink", call => \&preprocess_traillink, scan => 1);
19         hook(type => "pagetemplate", id => "trail", call => \&pagetemplate);
20 }
21
22 =head1 Page state
23
24 If a page C<$T> is a trail, then it can have
25
26 =over
27
28 =item * C<$pagestate{$T}{trail}{contents}>
29
30 Reference to an array of pagespecs or links in the trail.
31
32 =item * C<$pagestate{$T}{trail}{sort}>
33
34 A [[ikiwiki/pagespec/sorting]] order; if absent or undef, the trail is in
35 the order given by the links that form it
36
37 =item * C<$pagestate{$T}{trail}{circular}>
38
39 True if this trail is circular (i.e. going "next" from the last item is
40 allowed, and takes you back to the first)
41
42 =item * C<$pagestate{$T}{trail}{reverse}>
43
44 True if C<sort> is to be reversed.
45
46 =back
47
48 If a page C<$M> is a member of a trail C<$T>, then it has
49
50 =over
51
52 =item * C<$pagestate{$M}{trail}{item}{$T}[0]>
53
54 The page before this one in C<$T> at the last rebuild, or undef.
55
56 =item * C<$pagestate{$M}{trail}{item}{$T}[1]>
57
58 The page after this one in C<$T> at the last refresh, or undef.
59
60 =back
61
62 =cut
63
64 sub getsetup () {
65         return
66                 plugin => {
67                         safe => 1,
68                         rebuild => undef,
69                 },
70 }
71
72 sub needsbuild (@) {
73         my $needsbuild=shift;
74         foreach my $page (keys %pagestate) {
75                 if (exists $pagestate{$page}{trail}) {
76                         if (exists $pagesources{$page} &&
77                             grep { $_ eq $pagesources{$page} } @$needsbuild) {
78                                 # Remove state, it will be re-added
79                                 # if the preprocessor directive is still
80                                 # there during the rebuild. {item} is the
81                                 # only thing that's added for items, not
82                                 # trails, and it's harmless to delete that -
83                                 # the item is being rebuilt anyway.
84                                 delete $pagestate{$page}{trail};
85                         }
86                 }
87         }
88         return $needsbuild;
89 }
90
91 =for wiki
92
93 The `trail` directive is supplied by the [[plugins/contrib/trail]]
94 plugin. It sets options for the trail represented by this page. Example usage:
95
96     \[[!trail sort="meta(date)" circular="no" pages="blog/posts/*"]]
97
98 The available options are:
99
100 * `sort`: sets a [[ikiwiki/pagespec/sorting]] order; if not specified, the
101   items of the trail are ordered according to the first link to each item
102   found on the trail page
103
104 * `circular`: if set to `yes` or `1`, the trail is made into a loop by
105   making the last page's "next" link point to the first page, and the first
106   page's "previous" link point to the last page
107
108 * `pages`: add the given pages to the trail
109
110 =cut
111
112 sub preprocess_trail (@) {
113         my %params = @_;
114
115         if (exists $params{circular}) {
116                 $pagestate{$params{page}}{trail}{circular} =
117                         IkiWiki::yesno($params{circular});
118         }
119
120         if (exists $params{sort}) {
121                 $pagestate{$params{page}}{trail}{sort} = $params{sort};
122         }
123
124         if (exists $params{reverse}) {
125                 $pagestate{$params{page}}{trail}{reverse} = $params{reverse};
126         }
127
128         if (exists $params{pages}) {
129                 push @{$pagestate{$params{page}}{trail}{contents}}, "pagespec $params{pages}";
130         }
131
132         if (exists $params{pagenames}) {
133                 my @list = map { "link $_" } split ' ', $params{pagenames};
134                 push @{$pagestate{$params{page}}{trail}{contents}}, @list;
135         }
136
137         return "";
138 }
139
140 =for wiki
141
142 The `trailinline` directive is supplied by the [[plugins/contrib/trail]]
143 plugin. It behaves like the [[trail]] and [[inline]] directives combined.
144 Like [[inline]], it includes the selected pages into the page with the
145 directive and/or an RSS or Atom feed; like [[trail]], it turns the
146 included pages into a "trail" in which each page has a link to the
147 previous and next pages.
148
149     \[[!inline sort="meta(date)" circular="no" pages="blog/posts/*"]]
150
151 All the options for the [[inline]] and [[trail]] directives are valid.
152
153 The `show`, `skip` and `feedshow` options from [[inline]] do not apply
154 to the trail.
155
156 * `sort`: sets a [[ikiwiki/pagespec/sorting]] order; if not specified, the
157   items of the trail are ordered according to the first link to each item
158   found on the trail page
159
160 * `circular`: if set to `yes` or `1`, the trail is made into a loop by
161   making the last page's "next" link point to the first page, and the first
162   page's "previous" link point to the last page
163
164 * `pages`: add the given pages to the trail
165
166 =cut
167
168 sub preprocess_trailinline (@) {
169         preprocess_trail(@_);
170         return unless defined wantarray;
171
172         if (IkiWiki->can("preprocess_inline")) {
173                 return IkiWiki::preprocess_inline(@_);
174         }
175         else {
176                 error("trailinline directive requires the inline plugin");
177         }
178 }
179
180 =for wiki
181
182 The `trailitem` directive is supplied by the [[plugins/contrib/trail]] plugin.
183 It is used like this:
184
185     \[[!trailitem some_other_page]]
186
187 to add `some_other_page` to the trail represented by this page, without
188 generating a visible hyperlink.
189
190 =cut
191
192 sub preprocess_trailitem (@) {
193         my $link = shift;
194         shift;
195
196         my %params = @_;
197         my $trail = $params{page};
198
199         $link = linkpage($link);
200
201         add_link($params{page}, $link, 'trail');
202         push @{$pagestate{$params{page}}{trail}{contents}}, "link $link";
203
204         return "";
205 }
206
207 =for wiki
208
209 The `traillink` directive is supplied by the [[plugins/contrib/trail]] plugin.
210 It generates a visible [[ikiwiki/WikiLink]], and also adds the linked page to
211 the trail represented by the page containing the directive.
212
213 In its simplest form, the first parameter is like the content of a WikiLink:
214
215     \[[!traillink some_other_page]]
216
217 The displayed text can also be overridden, either with a `|` symbol or with
218 a `text` parameter:
219
220     \[[!traillink Click_here_to_start_the_trail|some_other_page]]
221     \[[!traillink some_other_page text="Click here to start the trail"]]
222
223 =cut
224
225 sub preprocess_traillink (@) {
226         my $link = shift;
227         shift;
228
229         my %params = @_;
230         my $trail = $params{page};
231
232         $link =~ qr{
233                         (?:
234                                 ([^\|]+)        # 1: link text
235                                 \|              # followed by |
236                         )?                      # optional
237
238                         (.+)                    # 2: page to link to
239                 }x;
240
241         my $linktext = $1;
242         $link = linkpage($2);
243
244         add_link($params{page}, $link, 'trail');
245         push @{$pagestate{$params{page}}{trail}{contents}}, "link $link";
246
247         if (defined $linktext) {
248                 $linktext = pagetitle($linktext);
249         }
250
251         if (exists $params{text}) {
252                 $linktext = $params{text};
253         }
254
255         if (defined $linktext) {
256                 return htmllink($trail, $params{destpage},
257                         $link, linktext => $linktext);
258         }
259
260         return htmllink($trail, $params{destpage}, $link);
261 }
262
263 # trail => [member1, member2]
264 my %trail_to_members;
265 # member => { trail => [prev, next] }
266 # e.g. if %trail_to_members = (
267 #       trail1 => ["member1", "member2"],
268 #       trail2 => ["member0", "member1"],
269 # )
270 #
271 # then $member_to_trails{member1} = {
272 #       trail1 => [undef, "member2"],
273 #       trail2 => ["member0", undef],
274 # }
275 my %member_to_trails;
276
277 # member => 1
278 my %rebuild_trail_members;
279
280 sub trails_differ {
281         my ($old, $new) = @_;
282
283         foreach my $trail (keys %$old) {
284                 if (! exists $new->{$trail}) {
285                         return 1;
286                 }
287                 my ($old_p, $old_n) = @{$old->{$trail}};
288                 my ($new_p, $new_n) = @{$new->{$trail}};
289                 $old_p = "" unless defined $old_p;
290                 $old_n = "" unless defined $old_n;
291                 $new_p = "" unless defined $new_p;
292                 $new_n = "" unless defined $new_n;
293                 if ($old_p ne $new_p) {
294                         return 1;
295                 }
296                 if ($old_n ne $new_n) {
297                         return 1;
298                 }
299         }
300
301         foreach my $trail (keys %$new) {
302                 if (! exists $old->{$trail}) {
303                         return 1;
304                 }
305         }
306
307         return 0;
308 }
309
310 my $done_prerender = 0;
311
312 my %origsubs;
313
314 sub prerender {
315         return if $done_prerender;
316
317         $origsubs{render_backlinks} = \&IkiWiki::render_backlinks;
318         inject(name => "IkiWiki::render_backlinks", call => \&render_backlinks);
319
320         %trail_to_members = ();
321         %member_to_trails = ();
322
323         foreach my $trail (keys %pagestate) {
324                 next unless exists $pagestate{$trail}{trail}{contents};
325
326                 my $members = [];
327                 my @contents = @{$pagestate{$trail}{trail}{contents}};
328
329
330                 foreach my $c (@contents) {
331                         if ($c =~ m/^pagespec (.*)$/) {
332                                 push @$members, pagespec_match_list($trail, $1);
333                         }
334                         elsif ($c =~ m/^link (.*)$/) {
335                                 my $best = bestlink($trail, $1);
336                                 push @$members, $best if length $best;
337                         }
338                 }
339
340                 if (defined $pagestate{$trail}{trail}{sort}) {
341                         # re-sort
342                         @$members = pagespec_match_list($trail, 'internal(*)',
343                                 list => $members,
344                                 sort => $pagestate{$trail}{trail}{sort});
345                 }
346
347                 if (IkiWiki::yesno $pagestate{$trail}{trail}{reverse}) {
348                         @$members = reverse @$members;
349                 }
350
351                 # uniquify
352                 my %seen;
353                 my @tmp;
354                 foreach my $member (@$members) {
355                         push @tmp, $member unless $seen{$member};
356                         $seen{$member} = 1;
357                 }
358                 $members = [@tmp];
359
360                 for (my $i = 0; $i <= $#$members; $i++) {
361                         my $member = $members->[$i];
362                         my $prev;
363                         $prev = $members->[$i - 1] if $i > 0;
364                         my $next = $members->[$i + 1];
365
366                         add_depends($member, $trail);
367
368                         $member_to_trails{$member}{$trail} = [$prev, $next];
369                 }
370
371                 if ((scalar @$members) > 1 && $pagestate{$trail}{trail}{circular}) {
372                         $member_to_trails{$members->[0]}{$trail}[0] = $members->[$#$members];
373                         $member_to_trails{$members->[$#$members]}{$trail}[1] = $members->[0];
374                 }
375
376                 $trail_to_members{$trail} = $members;
377         }
378
379         foreach my $member (keys %pagestate) {
380                 if (exists $pagestate{$member}{trail}{item} &&
381                         ! exists $member_to_trails{$member}) {
382                         $rebuild_trail_members{$member} = 1;
383                         delete $pagestate{$member}{trailitem};
384                 }
385         }
386
387         foreach my $member (keys %member_to_trails) {
388                 if (! exists $pagestate{$member}{trail}{item}) {
389                         $rebuild_trail_members{$member} = 1;
390                 }
391                 else {
392                         if (trails_differ($pagestate{$member}{trail}{item},
393                                         $member_to_trails{$member})) {
394                                 $rebuild_trail_members{$member} = 1;
395                         }
396                 }
397
398                 $pagestate{$member}{trail}{item} = $member_to_trails{$member};
399         }
400
401         $done_prerender = 1;
402 }
403
404 # This is called at about the right time that we can hijack it to render
405 # extra pages.
406 sub render_backlinks ($) {
407         my $blc = shift;
408
409         foreach my $member (keys %rebuild_trail_members) {
410                 next unless exists $pagesources{$member};
411
412                 IkiWiki::render($pagesources{$member}, sprintf(gettext("building %s, its previous or next page has changed"), $member));
413         }
414
415         $origsubs{render_backlinks}($blc);
416 }
417
418 sub title_of ($) {
419         my $page = shift;
420         if (defined ($pagestate{$page}{meta}{title})) {
421                 return $pagestate{$page}{meta}{title};
422         }
423         return pagetitle(IkiWiki::basename($page));
424 }
425
426 my $recursive = 0;
427
428 sub pagetemplate (@) {
429         my %params = @_;
430         my $page = $params{page};
431         my $template = $params{template};
432
433         if ($template->query(name => 'trails') && ! $recursive) {
434                 prerender();
435
436                 $recursive = 1;
437                 my $inner = template("trails.tmpl", blind_cache => 1);
438                 IkiWiki::run_hooks(pagetemplate => sub {
439                                 shift->(%params, template => $inner)
440                         });
441                 $template->param(trails => $inner->output);
442                 $recursive = 0;
443         }
444
445         if ($template->query(name => 'trailloop')) {
446                 prerender();
447
448                 my @trails;
449
450                 # sort backlinks by page name to have a consistent order
451                 foreach my $trail (sort keys %{$member_to_trails{$page}}) {
452
453                         my $members = $trail_to_members{$trail};
454                         my ($prev, $next) = @{$member_to_trails{$page}{$trail}};
455                         my ($prevurl, $nexturl, $prevtitle, $nexttitle);
456
457                         if (defined $prev) {
458                                 add_depends($params{destpage}, $prev);
459                                 $prevurl = urlto($prev, $page);
460                                 $prevtitle = title_of($prev);
461                         }
462
463                         if (defined $next) {
464                                 add_depends($params{destpage}, $next);
465                                 $nexturl = urlto($next, $page);
466                                 $nexttitle = title_of($next);
467                         }
468
469                         push @trails, {
470                                 prevpage => $prev,
471                                 prevtitle => $prevtitle,
472                                 prevurl => $prevurl,
473                                 nextpage => $next,
474                                 nexttitle => $nexttitle,
475                                 nexturl => $nexturl,
476                                 trailpage => $trail,
477                                 trailtitle => title_of($trail),
478                                 trailurl => urlto($trail, $page),
479                         };
480                 }
481
482                 $template->param(trailloop => \@trails);
483         }
484 }
485
486 1;