Replace [[!trailinline]] directive with [[!inline trail=yes]]
[ikiwiki] / IkiWiki / Plugin / trail.pm
1 #!/usr/bin/perl
2 # Copyright © 2008-2011 Joey Hess
3 # Copyright © 2009-2012 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 => "trailoptions", call => \&preprocess_trailoptions, scan => 1);
16         hook(type => "preprocess", id => "trailitem", call => \&preprocess_trailitem, scan => 1);
17         hook(type => "preprocess", id => "trailitems", call => \&preprocess_trailitems, 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_trailoptions (@) {
107         my %params = @_;
108
109         if (exists $params{circular}) {
110                 $pagestate{$params{page}}{trail}{circular} =
111                         IkiWiki::yesno($params{circular});
112         }
113
114         if (exists $params{sort}) {
115                 $pagestate{$params{page}}{trail}{sort} = $params{sort};
116         }
117
118         if (exists $params{reverse}) {
119                 $pagestate{$params{page}}{trail}{reverse} = $params{reverse};
120         }
121
122         return "";
123 }
124
125 sub preprocess_trailitem (@) {
126         my $link = shift;
127         shift;
128
129         # avoid collecting everything in the preprocess stage if we already
130         # did in the scan stage
131         if (defined wantarray) {
132                 return "" if $scanned;
133         }
134         else {
135                 $scanned = 1;
136         }
137
138         my %params = @_;
139         my $trail = $params{page};
140
141         $link = linkpage($link);
142
143         add_link($params{page}, $link, 'trail');
144         push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link];
145
146         return "";
147 }
148
149 sub preprocess_trailitems (@) {
150         my %params = @_;
151
152         # avoid collecting everything in the preprocess stage if we already
153         # did in the scan stage
154         if (defined wantarray) {
155                 return "" if $scanned;
156         }
157         else {
158                 $scanned = 1;
159         }
160
161         # trail members from a pagespec ought to be in some sort of order,
162         # and path is a nice obvious default
163         $params{sort} = 'path' unless exists $params{sort};
164         $params{reverse} = 'no' unless exists $params{reverse};
165
166         if (exists $params{pages}) {
167                 push @{$pagestate{$params{page}}{trail}{contents}},
168                         ["pagespec" => $params{pages}, $params{sort},
169                                 IkiWiki::yesno($params{reverse})];
170         }
171
172         if (exists $params{pagenames}) {
173                 my @list = map { [link =>  $_] } split ' ', $params{pagenames};
174                 push @{$pagestate{$params{page}}{trail}{contents}}, @list;
175         }
176
177         return "";
178 }
179
180 sub preprocess_traillink (@) {
181         my $link = shift;
182         shift;
183
184         my %params = @_;
185         my $trail = $params{page};
186
187         $link =~ qr{
188                         (?:
189                                 ([^\|]+)        # 1: link text
190                                 \|              # followed by |
191                         )?                      # optional
192
193                         (.+)                    # 2: page to link to
194                 }x;
195
196         my $linktext = $1;
197         $link = linkpage($2);
198
199         add_link($params{page}, $link, 'trail');
200
201         # avoid collecting everything in the preprocess stage if we already
202         # did in the scan stage
203         my $already;
204         if (defined wantarray) {
205                 $already = $scanned;
206         }
207         else {
208                 $scanned = 1;
209         }
210
211         push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link] unless $already;
212
213         if (defined $linktext) {
214                 $linktext = pagetitle($linktext);
215         }
216
217         if (exists $params{text}) {
218                 $linktext = $params{text};
219         }
220
221         if (defined $linktext) {
222                 return htmllink($trail, $params{destpage},
223                         $link, linktext => $linktext);
224         }
225
226         return htmllink($trail, $params{destpage}, $link);
227 }
228
229 # trail => [member1, member2]
230 my %trail_to_members;
231 # member => { trail => [prev, next] }
232 # e.g. if %trail_to_members = (
233 #       trail1 => ["member1", "member2"],
234 #       trail2 => ["member0", "member1"],
235 # )
236 #
237 # then $member_to_trails{member1} = {
238 #       trail1 => [undef, "member2"],
239 #       trail2 => ["member0", undef],
240 # }
241 my %member_to_trails;
242
243 # member => 1
244 my %rebuild_trail_members;
245
246 sub trails_differ {
247         my ($old, $new) = @_;
248
249         foreach my $trail (keys %$old) {
250                 if (! exists $new->{$trail}) {
251                         return 1;
252                 }
253                 my ($old_p, $old_n) = @{$old->{$trail}};
254                 my ($new_p, $new_n) = @{$new->{$trail}};
255                 $old_p = "" unless defined $old_p;
256                 $old_n = "" unless defined $old_n;
257                 $new_p = "" unless defined $new_p;
258                 $new_n = "" unless defined $new_n;
259                 if ($old_p ne $new_p) {
260                         return 1;
261                 }
262                 if ($old_n ne $new_n) {
263                         return 1;
264                 }
265         }
266
267         foreach my $trail (keys %$new) {
268                 if (! exists $old->{$trail}) {
269                         return 1;
270                 }
271         }
272
273         return 0;
274 }
275
276 my $done_prerender = 0;
277
278 my %origsubs;
279
280 sub prerender {
281         return if $done_prerender;
282
283         $origsubs{render_backlinks} = \&IkiWiki::render_backlinks;
284         inject(name => "IkiWiki::render_backlinks", call => \&render_backlinks);
285
286         %trail_to_members = ();
287         %member_to_trails = ();
288
289         foreach my $trail (keys %pagestate) {
290                 next unless exists $pagestate{$trail}{trail}{contents};
291
292                 my $members = [];
293                 my @contents = @{$pagestate{$trail}{trail}{contents}};
294
295                 foreach my $c (@contents) {
296                         if ($c->[0] eq 'pagespec') {
297                                 push @$members, pagespec_match_list($trail,
298                                         $c->[1], sort => $c->[2],
299                                         reverse => $c->[3]);
300                         }
301                         elsif ($c->[0] eq 'link') {
302                                 my $best = bestlink($trail, $c->[1]);
303                                 push @$members, $best if length $best;
304                         }
305                 }
306
307                 if (defined $pagestate{$trail}{trail}{sort}) {
308                         # re-sort
309                         @$members = pagespec_match_list($trail, 'internal(*)',
310                                 list => $members,
311                                 sort => $pagestate{$trail}{trail}{sort});
312                 }
313
314                 if (IkiWiki::yesno $pagestate{$trail}{trail}{reverse}) {
315                         @$members = reverse @$members;
316                 }
317
318                 # uniquify
319                 my %seen;
320                 my @tmp;
321                 foreach my $member (@$members) {
322                         push @tmp, $member unless $seen{$member};
323                         $seen{$member} = 1;
324                 }
325                 $members = [@tmp];
326
327                 for (my $i = 0; $i <= $#$members; $i++) {
328                         my $member = $members->[$i];
329                         my $prev;
330                         $prev = $members->[$i - 1] if $i > 0;
331                         my $next = $members->[$i + 1];
332
333                         add_depends($member, $trail);
334
335                         $member_to_trails{$member}{$trail} = [$prev, $next];
336                 }
337
338                 if ((scalar @$members) > 1 && $pagestate{$trail}{trail}{circular}) {
339                         $member_to_trails{$members->[0]}{$trail}[0] = $members->[$#$members];
340                         $member_to_trails{$members->[$#$members]}{$trail}[1] = $members->[0];
341                 }
342
343                 $trail_to_members{$trail} = $members;
344         }
345
346         foreach my $member (keys %pagestate) {
347                 if (exists $pagestate{$member}{trail}{item} &&
348                         ! exists $member_to_trails{$member}) {
349                         $rebuild_trail_members{$member} = 1;
350                         delete $pagestate{$member}{trailitem};
351                 }
352         }
353
354         foreach my $member (keys %member_to_trails) {
355                 if (! exists $pagestate{$member}{trail}{item}) {
356                         $rebuild_trail_members{$member} = 1;
357                 }
358                 else {
359                         if (trails_differ($pagestate{$member}{trail}{item},
360                                         $member_to_trails{$member})) {
361                                 $rebuild_trail_members{$member} = 1;
362                         }
363                 }
364
365                 $pagestate{$member}{trail}{item} = $member_to_trails{$member};
366         }
367
368         $done_prerender = 1;
369 }
370
371 # This is called at about the right time that we can hijack it to render
372 # extra pages.
373 sub render_backlinks ($) {
374         my $blc = shift;
375
376         foreach my $member (keys %rebuild_trail_members) {
377                 next unless exists $pagesources{$member};
378
379                 IkiWiki::render($pagesources{$member}, sprintf(gettext("building %s, its previous or next page has changed"), $member));
380         }
381
382         $origsubs{render_backlinks}($blc);
383 }
384
385 sub title_of ($) {
386         my $page = shift;
387         if (defined ($pagestate{$page}{meta}{title})) {
388                 return $pagestate{$page}{meta}{title};
389         }
390         return pagetitle(IkiWiki::basename($page));
391 }
392
393 my $recursive = 0;
394
395 sub pagetemplate (@) {
396         my %params = @_;
397         my $page = $params{page};
398         my $template = $params{template};
399
400         if ($template->query(name => 'trails') && ! $recursive) {
401                 prerender();
402
403                 $recursive = 1;
404                 my $inner = template("trails.tmpl", blind_cache => 1);
405                 IkiWiki::run_hooks(pagetemplate => sub {
406                                 shift->(%params, template => $inner)
407                         });
408                 $template->param(trails => $inner->output);
409                 $recursive = 0;
410         }
411
412         if ($template->query(name => 'trailloop')) {
413                 prerender();
414
415                 my @trails;
416
417                 # sort backlinks by page name to have a consistent order
418                 foreach my $trail (sort keys %{$member_to_trails{$page}}) {
419
420                         my $members = $trail_to_members{$trail};
421                         my ($prev, $next) = @{$member_to_trails{$page}{$trail}};
422                         my ($prevurl, $nexturl, $prevtitle, $nexttitle);
423
424                         if (defined $prev) {
425                                 add_depends($params{destpage}, $prev);
426                                 $prevurl = urlto($prev, $page);
427                                 $prevtitle = title_of($prev);
428                         }
429
430                         if (defined $next) {
431                                 add_depends($params{destpage}, $next);
432                                 $nexturl = urlto($next, $page);
433                                 $nexttitle = title_of($next);
434                         }
435
436                         push @trails, {
437                                 prevpage => $prev,
438                                 prevtitle => $prevtitle,
439                                 prevurl => $prevurl,
440                                 nextpage => $next,
441                                 nexttitle => $nexttitle,
442                                 nexturl => $nexturl,
443                                 trailpage => $trail,
444                                 trailtitle => title_of($trail),
445                                 trailurl => urlto($trail, $page),
446                         };
447                 }
448
449                 $template->param(trailloop => \@trails);
450         }
451 }
452
453 1;