trail: call prerender from build_affected
[ikiwiki.git] / 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         hook(type => "build_affected", id => "trail", call => \&build_affected);
21 }
22
23 # Page state
24
25 # If a page $T is a trail, then it can have
26
27 # * $pagestate{$T}{trail}{contents} 
28 #   Reference to an array of lists each containing either:
29 #     - [pagenames => "page1", "page2"]
30 #       Those literal pages
31 #     - [link => "link"]
32 #       A link specification, pointing to the same page that [[link]]
33 #       would select
34 #     - [pagespec => "posts/*", "age", 0]
35 #       A match by pagespec; the third array element is the sort order
36 #       and the fourth is whether to reverse sorting
37
38 # * $pagestate{$T}{trail}{sort}
39 #   A sorting order; if absent or undef, the trail is in the order given
40 #   by the links that form it
41 #
42 # * $pagestate{$T}{trail}{circular}
43 #   True if this trail is circular (i.e. going "next" from the last item is
44 #   allowed, and takes you back to the first)
45 #
46 # * $pagestate{$T}{trail}{reverse}
47 #   True if C<sort> is to be reversed.
48
49 # If a page $M is a member of a trail $T, then it has
50 #
51 # * $pagestate{$M}{trail}{item}{$T}[0]
52 #   The page before this one in C<$T> at the last rebuild, or undef.
53 #
54 # * $pagestate{$M}{trail}{item}{$T}[1]
55 #   The page after this one in C<$T> at the last refresh, or undef.
56
57 sub getsetup () {
58         return
59                 plugin => {
60                         safe => 1,
61                         rebuild => undef,
62                 },
63 }
64
65 sub needsbuild (@) {
66         my $needsbuild=shift;
67         foreach my $page (keys %pagestate) {
68                 if (exists $pagestate{$page}{trail}) {
69                         if (exists $pagesources{$page} &&
70                             grep { $_ eq $pagesources{$page} } @$needsbuild) {
71                                 # Remove state, it will be re-added
72                                 # if the preprocessor directive is still
73                                 # there during the rebuild. {item} is the
74                                 # only thing that's added for items, not
75                                 # trails, and it's harmless to delete that -
76                                 # the item is being rebuilt anyway.
77                                 delete $pagestate{$page}{trail};
78                         }
79                 }
80         }
81         return $needsbuild;
82 }
83
84 my $scanned = 0;
85
86 sub preprocess_trailoptions (@) {
87         my %params = @_;
88
89         if (exists $params{circular}) {
90                 $pagestate{$params{page}}{trail}{circular} =
91                         IkiWiki::yesno($params{circular});
92         }
93
94         if (exists $params{sort}) {
95                 $pagestate{$params{page}}{trail}{sort} = $params{sort};
96         }
97
98         if (exists $params{reverse}) {
99                 $pagestate{$params{page}}{trail}{reverse} = $params{reverse};
100         }
101
102         return "";
103 }
104
105 sub preprocess_trailitem (@) {
106         my $link = shift;
107         shift;
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         my %params = @_;
119         my $trail = $params{page};
120
121         $link = linkpage($link);
122
123         add_link($params{page}, $link, 'trail');
124         push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link];
125
126         return "";
127 }
128
129 sub preprocess_trailitems (@) {
130         my %params = @_;
131
132         # avoid collecting everything in the preprocess stage if we already
133         # did in the scan stage
134         if (defined wantarray) {
135                 return "" if $scanned;
136         }
137         else {
138                 $scanned = 1;
139         }
140
141         # trail members from a pagespec ought to be in some sort of order,
142         # and path is a nice obvious default
143         $params{sort} = 'path' unless exists $params{sort};
144         $params{reverse} = 'no' unless exists $params{reverse};
145
146         if (exists $params{pages}) {
147                 push @{$pagestate{$params{page}}{trail}{contents}},
148                         ["pagespec" => $params{pages}, $params{sort},
149                                 IkiWiki::yesno($params{reverse})];
150         }
151
152         if (exists $params{pagenames}) {
153                 push @{$pagestate{$params{page}}{trail}{contents}},
154                         [pagenames => (split ' ', $params{pagenames})];
155         }
156
157         return "";
158 }
159
160 sub preprocess_traillink (@) {
161         my $link = shift;
162         shift;
163
164         my %params = @_;
165         my $trail = $params{page};
166
167         $link =~ qr{
168                         (?:
169                                 ([^\|]+)        # 1: link text
170                                 \|              # followed by |
171                         )?                      # optional
172
173                         (.+)                    # 2: page to link to
174                 }x;
175
176         my $linktext = $1;
177         $link = linkpage($2);
178
179         add_link($params{page}, $link, 'trail');
180
181         # avoid collecting everything in the preprocess stage if we already
182         # did in the scan stage
183         my $already;
184         if (defined wantarray) {
185                 $already = $scanned;
186         }
187         else {
188                 $scanned = 1;
189         }
190
191         push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link] unless $already;
192
193         if (defined $linktext) {
194                 $linktext = pagetitle($linktext);
195         }
196
197         if (exists $params{text}) {
198                 $linktext = $params{text};
199         }
200
201         if (defined $linktext) {
202                 return htmllink($trail, $params{destpage},
203                         $link, linktext => $linktext);
204         }
205
206         return htmllink($trail, $params{destpage}, $link);
207 }
208
209 # trail => [member1, member2]
210 my %trail_to_members;
211 # member => { trail => [prev, next] }
212 # e.g. if %trail_to_members = (
213 #       trail1 => ["member1", "member2"],
214 #       trail2 => ["member0", "member1"],
215 # )
216 #
217 # then $member_to_trails{member1} = {
218 #       trail1 => [undef, "member2"],
219 #       trail2 => ["member0", undef],
220 # }
221 my %member_to_trails;
222
223 # member => 1
224 my %rebuild_trail_members;
225
226 sub trails_differ {
227         my ($old, $new) = @_;
228
229         foreach my $trail (keys %$old) {
230                 if (! exists $new->{$trail}) {
231                         return 1;
232                 }
233                 my ($old_p, $old_n) = @{$old->{$trail}};
234                 my ($new_p, $new_n) = @{$new->{$trail}};
235                 $old_p = "" unless defined $old_p;
236                 $old_n = "" unless defined $old_n;
237                 $new_p = "" unless defined $new_p;
238                 $new_n = "" unless defined $new_n;
239                 if ($old_p ne $new_p) {
240                         return 1;
241                 }
242                 if ($old_n ne $new_n) {
243                         return 1;
244                 }
245         }
246
247         foreach my $trail (keys %$new) {
248                 if (! exists $old->{$trail}) {
249                         return 1;
250                 }
251         }
252
253         return 0;
254 }
255
256 my $done_prerender = 0;
257
258 sub prerender {
259         return if $done_prerender;
260
261         %trail_to_members = ();
262         %member_to_trails = ();
263
264         foreach my $trail (keys %pagestate) {
265                 next unless exists $pagestate{$trail}{trail}{contents};
266
267                 my $members = [];
268                 my @contents = @{$pagestate{$trail}{trail}{contents}};
269
270                 foreach my $c (@contents) {
271                         if ($c->[0] eq 'pagespec') {
272                                 push @$members, pagespec_match_list($trail,
273                                         $c->[1], sort => $c->[2],
274                                         reverse => $c->[3]);
275                         }
276                         elsif ($c->[0] eq 'pagenames') {
277                                 my @pagenames = @$c;
278                                 shift @pagenames;
279                                 foreach my $page (@pagenames) {
280                                         if (exists $pagesources{$page}) {
281                                                 push @$members, $page;
282                                         }
283                                         else {
284                                                 # rebuild trail if it turns up
285                                                 add_depends($trail, $page, deptype("presence"));
286                                         }
287                                 }
288                         }
289                         elsif ($c->[0] eq 'link') {
290                                 my $best = bestlink($trail, $c->[1]);
291                                 push @$members, $best if length $best;
292                         }
293                 }
294
295                 if (defined $pagestate{$trail}{trail}{sort}) {
296                         # re-sort
297                         @$members = pagespec_match_list($trail, 'internal(*)',
298                                 list => $members,
299                                 sort => $pagestate{$trail}{trail}{sort});
300                 }
301
302                 if (IkiWiki::yesno $pagestate{$trail}{trail}{reverse}) {
303                         @$members = reverse @$members;
304                 }
305
306                 # uniquify
307                 my %seen;
308                 my @tmp;
309                 foreach my $member (@$members) {
310                         push @tmp, $member unless $seen{$member};
311                         $seen{$member} = 1;
312                 }
313                 $members = [@tmp];
314
315                 for (my $i = 0; $i <= $#$members; $i++) {
316                         my $member = $members->[$i];
317                         my $prev;
318                         $prev = $members->[$i - 1] if $i > 0;
319                         my $next = $members->[$i + 1];
320
321                         add_depends($member, $trail, deptype("presence"));
322
323                         $member_to_trails{$member}{$trail} = [$prev, $next];
324                 }
325
326                 if ((scalar @$members) > 1 && $pagestate{$trail}{trail}{circular}) {
327                         $member_to_trails{$members->[0]}{$trail}[0] = $members->[$#$members];
328                         $member_to_trails{$members->[$#$members]}{$trail}[1] = $members->[0];
329                 }
330
331                 $trail_to_members{$trail} = $members;
332         }
333
334         foreach my $member (keys %pagestate) {
335                 if (exists $pagestate{$member}{trail}{item} &&
336                         ! exists $member_to_trails{$member}) {
337                         $rebuild_trail_members{$member} = 1;
338                         delete $pagestate{$member}{trail}{item};
339                 }
340         }
341
342         foreach my $member (keys %member_to_trails) {
343                 if (! exists $pagestate{$member}{trail}{item}) {
344                         $rebuild_trail_members{$member} = 1;
345                 }
346                 else {
347                         if (trails_differ($pagestate{$member}{trail}{item},
348                                         $member_to_trails{$member})) {
349                                 $rebuild_trail_members{$member} = 1;
350                         }
351                 }
352
353                 $pagestate{$member}{trail}{item} = $member_to_trails{$member};
354         }
355
356         $done_prerender = 1;
357 }
358
359 sub build_affected {
360         my %affected;
361
362         # In principle we might not have done this yet, although in practice
363         # at least the trail itself has probably changed, and its template
364         # almost certainly contains TRAILS or TRAILLOOP, triggering our
365         # prerender as a side-effect.
366         prerender();
367
368         foreach my $member (keys %rebuild_trail_members) {
369                 $affected{$member} = sprintf(gettext("building %s, its previous or next page has changed"), $member);
370         }
371
372         return %affected;
373 }
374
375 sub title_of ($) {
376         my $page = shift;
377         if (defined ($pagestate{$page}{meta}{title})) {
378                 return $pagestate{$page}{meta}{title};
379         }
380         return pagetitle(IkiWiki::basename($page));
381 }
382
383 my $recursive = 0;
384
385 sub pagetemplate (@) {
386         my %params = @_;
387         my $page = $params{page};
388         my $template = $params{template};
389
390         if ($template->query(name => 'trails') && ! $recursive) {
391                 prerender();
392
393                 $recursive = 1;
394                 my $inner = template("trails.tmpl", blind_cache => 1);
395                 IkiWiki::run_hooks(pagetemplate => sub {
396                                 shift->(%params, template => $inner)
397                         });
398                 $template->param(trails => $inner->output);
399                 $recursive = 0;
400         }
401
402         if ($template->query(name => 'trailloop')) {
403                 prerender();
404
405                 my @trails;
406
407                 # sort backlinks by page name to have a consistent order
408                 foreach my $trail (sort keys %{$member_to_trails{$page}}) {
409
410                         my $members = $trail_to_members{$trail};
411                         my ($prev, $next) = @{$member_to_trails{$page}{$trail}};
412                         my ($prevurl, $nexturl, $prevtitle, $nexttitle);
413
414                         if (defined $prev) {
415                                 add_depends($params{destpage}, $prev, deptype("presence"));
416                                 $prevurl = urlto($prev, $page);
417                                 $prevtitle = title_of($prev);
418                         }
419
420                         if (defined $next) {
421                                 add_depends($params{destpage}, $next, deptype("presence"));
422                                 $nexturl = urlto($next, $page);
423                                 $nexttitle = title_of($next);
424                         }
425
426                         push @trails, {
427                                 prevpage => $prev,
428                                 prevtitle => $prevtitle,
429                                 prevurl => $prevurl,
430                                 nextpage => $next,
431                                 nexttitle => $nexttitle,
432                                 nexturl => $nexturl,
433                                 trailpage => $trail,
434                                 trailtitle => title_of($trail),
435                                 trailurl => urlto($trail, $page),
436                         };
437                 }
438
439                 $template->param(trailloop => \@trails);
440         }
441 }
442
443 1;