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