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;
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);
25 # If a page $T is a trail, then it can have
27 # * $pagestate{$T}{trail}{contents}
28 # Reference to an array of lists each containing either:
29 # - [pagenames => "page1", "page2"]
32 # A link specification, pointing to the same page that [[link]]
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
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
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)
46 # * $pagestate{$T}{trail}{reverse}
47 # True if C<sort> is to be reversed.
49 # If a page $M is a member of a trail $T, then it has
51 # * $pagestate{$M}{trail}{item}{$T}[0]
52 # The page before this one in C<$T> at the last rebuild, or undef.
54 # * $pagestate{$M}{trail}{item}{$T}[1]
55 # The page after this one in C<$T> at the last refresh, or undef.
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};
86 sub preprocess_trailoptions (@) {
89 if (exists $params{circular}) {
90 $pagestate{$params{page}}{trail}{circular} =
91 IkiWiki::yesno($params{circular});
94 if (exists $params{sort}) {
95 $pagestate{$params{page}}{trail}{sort} = $params{sort};
98 if (exists $params{reverse}) {
99 $pagestate{$params{page}}{trail}{reverse} = $params{reverse};
105 sub preprocess_trailitem (@) {
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;
119 my $trail = $params{page};
121 $link = linkpage($link);
123 add_link($params{page}, $link, 'trail');
124 push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link];
129 sub preprocess_trailitems (@) {
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;
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};
146 if (exists $params{pages}) {
147 push @{$pagestate{$params{page}}{trail}{contents}},
148 ["pagespec" => $params{pages}, $params{sort},
149 IkiWiki::yesno($params{reverse})];
152 if (exists $params{pagenames}) {
153 push @{$pagestate{$params{page}}{trail}{contents}},
154 [pagenames => (split ' ', $params{pagenames})];
160 sub preprocess_traillink (@) {
165 my $trail = $params{page};
169 ([^\|]+) # 1: link text
173 (.+) # 2: page to link to
177 $link = linkpage($2);
179 add_link($params{page}, $link, 'trail');
181 # avoid collecting everything in the preprocess stage if we already
182 # did in the scan stage
184 if (defined wantarray) {
191 push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link] unless $already;
193 if (defined $linktext) {
194 $linktext = pagetitle($linktext);
197 if (exists $params{text}) {
198 $linktext = $params{text};
201 if (defined $linktext) {
202 return htmllink($trail, $params{destpage},
203 $link, linktext => $linktext);
206 return htmllink($trail, $params{destpage}, $link);
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"],
217 # then $member_to_trails{member1} = {
218 # trail1 => [undef, "member2"],
219 # trail2 => ["member0", undef],
221 my %member_to_trails;
224 my %rebuild_trail_members;
227 my ($old, $new) = @_;
229 foreach my $trail (keys %$old) {
230 if (! exists $new->{$trail}) {
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) {
242 if ($old_n ne $new_n) {
247 foreach my $trail (keys %$new) {
248 if (! exists $old->{$trail}) {
256 my $done_prerender = 0;
259 return if $done_prerender;
261 %trail_to_members = ();
262 %member_to_trails = ();
264 foreach my $trail (keys %pagestate) {
265 next unless exists $pagestate{$trail}{trail}{contents};
268 my @contents = @{$pagestate{$trail}{trail}{contents}};
270 foreach my $c (@contents) {
271 if ($c->[0] eq 'pagespec') {
272 push @$members, pagespec_match_list($trail,
273 $c->[1], sort => $c->[2],
276 elsif ($c->[0] eq 'pagenames') {
279 foreach my $page (@pagenames) {
280 if (exists $pagesources{$page}) {
281 push @$members, $page;
284 # rebuild trail if it turns up
285 add_depends($trail, $page, deptype("presence"));
289 elsif ($c->[0] eq 'link') {
290 my $best = bestlink($trail, $c->[1]);
291 push @$members, $best if length $best;
295 if (defined $pagestate{$trail}{trail}{sort}) {
297 @$members = pagespec_match_list($trail, 'internal(*)',
299 sort => $pagestate{$trail}{trail}{sort});
302 if (IkiWiki::yesno $pagestate{$trail}{trail}{reverse}) {
303 @$members = reverse @$members;
309 foreach my $member (@$members) {
310 push @tmp, $member unless $seen{$member};
315 for (my $i = 0; $i <= $#$members; $i++) {
316 my $member = $members->[$i];
318 $prev = $members->[$i - 1] if $i > 0;
319 my $next = $members->[$i + 1];
321 add_depends($member, $trail);
323 $member_to_trails{$member}{$trail} = [$prev, $next];
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];
331 $trail_to_members{$trail} = $members;
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}{trailitem};
342 foreach my $member (keys %member_to_trails) {
343 if (! exists $pagestate{$member}{trail}{item}) {
344 $rebuild_trail_members{$member} = 1;
347 if (trails_differ($pagestate{$member}{trail}{item},
348 $member_to_trails{$member})) {
349 $rebuild_trail_members{$member} = 1;
353 $pagestate{$member}{trail}{item} = $member_to_trails{$member};
362 foreach my $member (keys %rebuild_trail_members) {
363 $affected{$member} = sprintf(gettext("building %s, its previous or next page has changed"), $member);
371 if (defined ($pagestate{$page}{meta}{title})) {
372 return $pagestate{$page}{meta}{title};
374 return pagetitle(IkiWiki::basename($page));
379 sub pagetemplate (@) {
381 my $page = $params{page};
382 my $template = $params{template};
384 if ($template->query(name => 'trails') && ! $recursive) {
388 my $inner = template("trails.tmpl", blind_cache => 1);
389 IkiWiki::run_hooks(pagetemplate => sub {
390 shift->(%params, template => $inner)
392 $template->param(trails => $inner->output);
396 if ($template->query(name => 'trailloop')) {
401 # sort backlinks by page name to have a consistent order
402 foreach my $trail (sort keys %{$member_to_trails{$page}}) {
404 my $members = $trail_to_members{$trail};
405 my ($prev, $next) = @{$member_to_trails{$page}{$trail}};
406 my ($prevurl, $nexturl, $prevtitle, $nexttitle);
409 add_depends($params{destpage}, $prev);
410 $prevurl = urlto($prev, $page);
411 $prevtitle = title_of($prev);
415 add_depends($params{destpage}, $next);
416 $nexturl = urlto($next, $page);
417 $nexttitle = title_of($next);
422 prevtitle => $prevtitle,
425 nexttitle => $nexttitle,
428 trailtitle => title_of($trail),
429 trailurl => urlto($trail, $page),
433 $template->param(trailloop => \@trails);