trail: update documentation; drop docs for directives, which are now in the wiki
[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 lists each containing either:
31
32 =over
33
34 =item * C<[link, "link"]>
35
36 A link specification, pointing to the same page that C<[[link]]> would select
37
38 =item * C<[pagespec, "posts/*", "age", 0]>
39
40 A match by pagespec; the third array element is the sort order and the fourth
41 is whether to reverse sorting
42
43 =back
44
45 =item * C<$pagestate{$T}{trail}{sort}>
46
47 A [[ikiwiki/pagespec/sorting]] order; if absent or undef, the trail is in
48 the order given by the links that form it
49
50 =item * C<$pagestate{$T}{trail}{circular}>
51
52 True if this trail is circular (i.e. going "next" from the last item is
53 allowed, and takes you back to the first)
54
55 =item * C<$pagestate{$T}{trail}{reverse}>
56
57 True if C<sort> is to be reversed.
58
59 =back
60
61 If a page C<$M> is a member of a trail C<$T>, then it has
62
63 =over
64
65 =item * C<$pagestate{$M}{trail}{item}{$T}[0]>
66
67 The page before this one in C<$T> at the last rebuild, or undef.
68
69 =item * C<$pagestate{$M}{trail}{item}{$T}[1]>
70
71 The page after this one in C<$T> at the last refresh, or undef.
72
73 =back
74
75 =cut
76
77 sub getsetup () {
78         return
79                 plugin => {
80                         safe => 1,
81                         rebuild => undef,
82                 },
83 }
84
85 sub needsbuild (@) {
86         my $needsbuild=shift;
87         foreach my $page (keys %pagestate) {
88                 if (exists $pagestate{$page}{trail}) {
89                         if (exists $pagesources{$page} &&
90                             grep { $_ eq $pagesources{$page} } @$needsbuild) {
91                                 # Remove state, it will be re-added
92                                 # if the preprocessor directive is still
93                                 # there during the rebuild. {item} is the
94                                 # only thing that's added for items, not
95                                 # trails, and it's harmless to delete that -
96                                 # the item is being rebuilt anyway.
97                                 delete $pagestate{$page}{trail};
98                         }
99                 }
100         }
101         return $needsbuild;
102 }
103
104 my $scanned = 0;
105
106 sub preprocess_trail (@) {
107         my %params = @_;
108
109         # avoid collecting everything in the preprocess stage if we already
110         # did in the scan stage
111         if (defined wantarray) {
112                 return "" if $scanned;
113         }
114         else {
115                 $scanned = 1;
116         }
117
118         # trail members from a pagespec ought to be in some sort of order,
119         # and path is a nice obvious default
120         $params{sortthese} = 'path' unless exists $params{sortthese};
121         $params{reversethese} = 'no' unless exists $params{reversethese};
122
123         if (exists $params{circular}) {
124                 $pagestate{$params{page}}{trail}{circular} =
125                         IkiWiki::yesno($params{circular});
126         }
127
128         if (exists $params{sort}) {
129                 $pagestate{$params{page}}{trail}{sort} = $params{sort};
130         }
131
132         if (exists $params{reverse}) {
133                 $pagestate{$params{page}}{trail}{reverse} = $params{reverse};
134         }
135
136         if (exists $params{pages}) {
137                 push @{$pagestate{$params{page}}{trail}{contents}},
138                         ["pagespec" => $params{pages}, $params{sortthese},
139                                 IkiWiki::yesno($params{reversethese})];
140         }
141
142         if (exists $params{pagenames}) {
143                 my @list = map { [link =>  $_] } split ' ', $params{pagenames};
144                 push @{$pagestate{$params{page}}{trail}{contents}}, @list;
145         }
146
147         return "";
148 }
149
150 sub preprocess_trailinline (@) {
151         my %params = @_;
152
153         if (exists $params{sort}) {
154                 $params{sortthese} = $params{sort};
155                 delete $params{sort};
156         }
157         else {
158                 # sort in the same order as [[plugins/inline]]'s default
159                 $params{sortthese} = 'age';
160         }
161
162         if (exists $params{reverse}) {
163                 $params{reversethese} = $params{reverse};
164                 delete $params{reverse};
165         }
166
167         if (exists $params{trailsort}) {
168                 $params{sort} = $params{trailsort};
169         }
170
171         if (exists $params{trailreverse}) {
172                 $params{reverse} = $params{trailreverse};
173         }
174
175         if (defined wantarray) {
176                 scalar preprocess_trail(%params);
177
178                 if (IkiWiki->can("preprocess_inline")) {
179                         return IkiWiki::preprocess_inline(@_);
180                 }
181                 else {
182                         error("trailinline directive requires the inline plugin");
183                 }
184         }
185         else {
186                 preprocess_trail(%params);
187         }
188 }
189
190 sub preprocess_trailitem (@) {
191         my $link = shift;
192         shift;
193
194         # avoid collecting everything in the preprocess stage if we already
195         # did in the scan stage
196         if (defined wantarray) {
197                 return "" if $scanned;
198         }
199         else {
200                 $scanned = 1;
201         }
202
203         my %params = @_;
204         my $trail = $params{page};
205
206         $link = linkpage($link);
207
208         add_link($params{page}, $link, 'trail');
209         push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link];
210
211         return "";
212 }
213
214 sub preprocess_traillink (@) {
215         my $link = shift;
216         shift;
217
218         my %params = @_;
219         my $trail = $params{page};
220
221         $link =~ qr{
222                         (?:
223                                 ([^\|]+)        # 1: link text
224                                 \|              # followed by |
225                         )?                      # optional
226
227                         (.+)                    # 2: page to link to
228                 }x;
229
230         my $linktext = $1;
231         $link = linkpage($2);
232
233         add_link($params{page}, $link, 'trail');
234
235         # avoid collecting everything in the preprocess stage if we already
236         # did in the scan stage
237         my $already;
238         if (defined wantarray) {
239                 $already = $scanned;
240         }
241         else {
242                 $scanned = 1;
243         }
244
245         push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link] unless $already;
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                 foreach my $c (@contents) {
330                         if ($c->[0] eq 'pagespec') {
331                                 push @$members, pagespec_match_list($trail,
332                                         $c->[1], sort => $c->[2],
333                                         reverse => $c->[3]);
334                         }
335                         elsif ($c->[0] eq 'link') {
336                                 my $best = bestlink($trail, $c->[1]);
337                                 push @$members, $best if length $best;
338                         }
339                 }
340
341                 if (defined $pagestate{$trail}{trail}{sort}) {
342                         # re-sort
343                         @$members = pagespec_match_list($trail, 'internal(*)',
344                                 list => $members,
345                                 sort => $pagestate{$trail}{trail}{sort});
346                 }
347
348                 if (IkiWiki::yesno $pagestate{$trail}{trail}{reverse}) {
349                         @$members = reverse @$members;
350                 }
351
352                 # uniquify
353                 my %seen;
354                 my @tmp;
355                 foreach my $member (@$members) {
356                         push @tmp, $member unless $seen{$member};
357                         $seen{$member} = 1;
358                 }
359                 $members = [@tmp];
360
361                 for (my $i = 0; $i <= $#$members; $i++) {
362                         my $member = $members->[$i];
363                         my $prev;
364                         $prev = $members->[$i - 1] if $i > 0;
365                         my $next = $members->[$i + 1];
366
367                         add_depends($member, $trail);
368
369                         $member_to_trails{$member}{$trail} = [$prev, $next];
370                 }
371
372                 if ((scalar @$members) > 1 && $pagestate{$trail}{trail}{circular}) {
373                         $member_to_trails{$members->[0]}{$trail}[0] = $members->[$#$members];
374                         $member_to_trails{$members->[$#$members]}{$trail}[1] = $members->[0];
375                 }
376
377                 $trail_to_members{$trail} = $members;
378         }
379
380         foreach my $member (keys %pagestate) {
381                 if (exists $pagestate{$member}{trail}{item} &&
382                         ! exists $member_to_trails{$member}) {
383                         $rebuild_trail_members{$member} = 1;
384                         delete $pagestate{$member}{trailitem};
385                 }
386         }
387
388         foreach my $member (keys %member_to_trails) {
389                 if (! exists $pagestate{$member}{trail}{item}) {
390                         $rebuild_trail_members{$member} = 1;
391                 }
392                 else {
393                         if (trails_differ($pagestate{$member}{trail}{item},
394                                         $member_to_trails{$member})) {
395                                 $rebuild_trail_members{$member} = 1;
396                         }
397                 }
398
399                 $pagestate{$member}{trail}{item} = $member_to_trails{$member};
400         }
401
402         $done_prerender = 1;
403 }
404
405 # This is called at about the right time that we can hijack it to render
406 # extra pages.
407 sub render_backlinks ($) {
408         my $blc = shift;
409
410         foreach my $member (keys %rebuild_trail_members) {
411                 next unless exists $pagesources{$member};
412
413                 IkiWiki::render($pagesources{$member}, sprintf(gettext("building %s, its previous or next page has changed"), $member));
414         }
415
416         $origsubs{render_backlinks}($blc);
417 }
418
419 sub title_of ($) {
420         my $page = shift;
421         if (defined ($pagestate{$page}{meta}{title})) {
422                 return $pagestate{$page}{meta}{title};
423         }
424         return pagetitle(IkiWiki::basename($page));
425 }
426
427 my $recursive = 0;
428
429 sub pagetemplate (@) {
430         my %params = @_;
431         my $page = $params{page};
432         my $template = $params{template};
433
434         if ($template->query(name => 'trails') && ! $recursive) {
435                 prerender();
436
437                 $recursive = 1;
438                 my $inner = template("trails.tmpl", blind_cache => 1);
439                 IkiWiki::run_hooks(pagetemplate => sub {
440                                 shift->(%params, template => $inner)
441                         });
442                 $template->param(trails => $inner->output);
443                 $recursive = 0;
444         }
445
446         if ($template->query(name => 'trailloop')) {
447                 prerender();
448
449                 my @trails;
450
451                 # sort backlinks by page name to have a consistent order
452                 foreach my $trail (sort keys %{$member_to_trails{$page}}) {
453
454                         my $members = $trail_to_members{$trail};
455                         my ($prev, $next) = @{$member_to_trails{$page}{$trail}};
456                         my ($prevurl, $nexturl, $prevtitle, $nexttitle);
457
458                         if (defined $prev) {
459                                 add_depends($params{destpage}, $prev);
460                                 $prevurl = urlto($prev, $page);
461                                 $prevtitle = title_of($prev);
462                         }
463
464                         if (defined $next) {
465                                 add_depends($params{destpage}, $next);
466                                 $nexturl = urlto($next, $page);
467                                 $nexttitle = title_of($next);
468                         }
469
470                         push @trails, {
471                                 prevpage => $prev,
472                                 prevtitle => $prevtitle,
473                                 prevurl => $prevurl,
474                                 nextpage => $next,
475                                 nexttitle => $nexttitle,
476                                 nexturl => $nexturl,
477                                 trailpage => $trail,
478                                 trailtitle => title_of($trail),
479                                 trailurl => urlto($trail, $page),
480                         };
481                 }
482
483                 $template->param(trailloop => \@trails);
484         }
485 }
486
487 1;