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;
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);
24 If a page C<$T> is a trail, then it can have
28 =item * C<$pagestate{$T}{trail}{contents}>
30 Reference to an array of lists each containing either:
34 =item * C<[link, "link"]>
36 A link specification, pointing to the same page that C<[[link]]> would select
38 =item * C<[pagespec, "posts/*", "age", 0]>
40 A match by pagespec; the third array element is the sort order and the fourth
41 is whether to reverse sorting
45 =item * C<$pagestate{$T}{trail}{sort}>
47 A [[ikiwiki/pagespec/sorting]] order; if absent or undef, the trail is in
48 the order given by the links that form it
50 =item * C<$pagestate{$T}{trail}{circular}>
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)
55 =item * C<$pagestate{$T}{trail}{reverse}>
57 True if C<sort> is to be reversed.
61 If a page C<$M> is a member of a trail C<$T>, then it has
65 =item * C<$pagestate{$M}{trail}{item}{$T}[0]>
67 The page before this one in C<$T> at the last rebuild, or undef.
69 =item * C<$pagestate{$M}{trail}{item}{$T}[1]>
71 The page after this one in C<$T> at the last refresh, or undef.
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};
106 sub preprocess_trail (@) {
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;
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};
123 if (exists $params{circular}) {
124 $pagestate{$params{page}}{trail}{circular} =
125 IkiWiki::yesno($params{circular});
128 if (exists $params{sort}) {
129 $pagestate{$params{page}}{trail}{sort} = $params{sort};
132 if (exists $params{reverse}) {
133 $pagestate{$params{page}}{trail}{reverse} = $params{reverse};
136 if (exists $params{pages}) {
137 push @{$pagestate{$params{page}}{trail}{contents}},
138 ["pagespec" => $params{pages}, $params{sortthese},
139 IkiWiki::yesno($params{reversethese})];
142 if (exists $params{pagenames}) {
143 my @list = map { [link => $_] } split ' ', $params{pagenames};
144 push @{$pagestate{$params{page}}{trail}{contents}}, @list;
150 sub preprocess_trailinline (@) {
153 if (exists $params{sort}) {
154 $params{sortthese} = $params{sort};
155 delete $params{sort};
158 # sort in the same order as [[plugins/inline]]'s default
159 $params{sortthese} = 'age';
162 if (exists $params{reverse}) {
163 $params{reversethese} = $params{reverse};
164 delete $params{reverse};
167 if (exists $params{trailsort}) {
168 $params{sort} = $params{trailsort};
171 if (exists $params{trailreverse}) {
172 $params{reverse} = $params{trailreverse};
175 if (defined wantarray) {
176 scalar preprocess_trail(%params);
178 if (IkiWiki->can("preprocess_inline")) {
179 return IkiWiki::preprocess_inline(@_);
182 error("trailinline directive requires the inline plugin");
186 preprocess_trail(%params);
190 sub preprocess_trailitem (@) {
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;
204 my $trail = $params{page};
206 $link = linkpage($link);
208 add_link($params{page}, $link, 'trail');
209 push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link];
214 sub preprocess_traillink (@) {
219 my $trail = $params{page};
223 ([^\|]+) # 1: link text
227 (.+) # 2: page to link to
231 $link = linkpage($2);
233 add_link($params{page}, $link, 'trail');
235 # avoid collecting everything in the preprocess stage if we already
236 # did in the scan stage
238 if (defined wantarray) {
245 push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link] unless $already;
247 if (defined $linktext) {
248 $linktext = pagetitle($linktext);
251 if (exists $params{text}) {
252 $linktext = $params{text};
255 if (defined $linktext) {
256 return htmllink($trail, $params{destpage},
257 $link, linktext => $linktext);
260 return htmllink($trail, $params{destpage}, $link);
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"],
271 # then $member_to_trails{member1} = {
272 # trail1 => [undef, "member2"],
273 # trail2 => ["member0", undef],
275 my %member_to_trails;
278 my %rebuild_trail_members;
281 my ($old, $new) = @_;
283 foreach my $trail (keys %$old) {
284 if (! exists $new->{$trail}) {
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) {
296 if ($old_n ne $new_n) {
301 foreach my $trail (keys %$new) {
302 if (! exists $old->{$trail}) {
310 my $done_prerender = 0;
315 return if $done_prerender;
317 $origsubs{render_backlinks} = \&IkiWiki::render_backlinks;
318 inject(name => "IkiWiki::render_backlinks", call => \&render_backlinks);
320 %trail_to_members = ();
321 %member_to_trails = ();
323 foreach my $trail (keys %pagestate) {
324 next unless exists $pagestate{$trail}{trail}{contents};
327 my @contents = @{$pagestate{$trail}{trail}{contents}};
329 foreach my $c (@contents) {
330 if ($c->[0] eq 'pagespec') {
331 push @$members, pagespec_match_list($trail,
332 $c->[1], sort => $c->[2],
335 elsif ($c->[0] eq 'link') {
336 my $best = bestlink($trail, $c->[1]);
337 push @$members, $best if length $best;
341 if (defined $pagestate{$trail}{trail}{sort}) {
343 @$members = pagespec_match_list($trail, 'internal(*)',
345 sort => $pagestate{$trail}{trail}{sort});
348 if (IkiWiki::yesno $pagestate{$trail}{trail}{reverse}) {
349 @$members = reverse @$members;
355 foreach my $member (@$members) {
356 push @tmp, $member unless $seen{$member};
361 for (my $i = 0; $i <= $#$members; $i++) {
362 my $member = $members->[$i];
364 $prev = $members->[$i - 1] if $i > 0;
365 my $next = $members->[$i + 1];
367 add_depends($member, $trail);
369 $member_to_trails{$member}{$trail} = [$prev, $next];
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];
377 $trail_to_members{$trail} = $members;
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};
388 foreach my $member (keys %member_to_trails) {
389 if (! exists $pagestate{$member}{trail}{item}) {
390 $rebuild_trail_members{$member} = 1;
393 if (trails_differ($pagestate{$member}{trail}{item},
394 $member_to_trails{$member})) {
395 $rebuild_trail_members{$member} = 1;
399 $pagestate{$member}{trail}{item} = $member_to_trails{$member};
405 # This is called at about the right time that we can hijack it to render
407 sub render_backlinks ($) {
410 foreach my $member (keys %rebuild_trail_members) {
411 next unless exists $pagesources{$member};
413 IkiWiki::render($pagesources{$member}, sprintf(gettext("building %s, its previous or next page has changed"), $member));
416 $origsubs{render_backlinks}($blc);
421 if (defined ($pagestate{$page}{meta}{title})) {
422 return $pagestate{$page}{meta}{title};
424 return pagetitle(IkiWiki::basename($page));
429 sub pagetemplate (@) {
431 my $page = $params{page};
432 my $template = $params{template};
434 if ($template->query(name => 'trails') && ! $recursive) {
438 my $inner = template("trails.tmpl", blind_cache => 1);
439 IkiWiki::run_hooks(pagetemplate => sub {
440 shift->(%params, template => $inner)
442 $template->param(trails => $inner->output);
446 if ($template->query(name => 'trailloop')) {
451 # sort backlinks by page name to have a consistent order
452 foreach my $trail (sort keys %{$member_to_trails{$page}}) {
454 my $members = $trail_to_members{$trail};
455 my ($prev, $next) = @{$member_to_trails{$page}{$trail}};
456 my ($prevurl, $nexturl, $prevtitle, $nexttitle);
459 add_depends($params{destpage}, $prev);
460 $prevurl = urlto($prev, $page);
461 $prevtitle = title_of($prev);
465 add_depends($params{destpage}, $next);
466 $nexturl = urlto($next, $page);
467 $nexttitle = title_of($next);
472 prevtitle => $prevtitle,
475 nexttitle => $nexttitle,
478 trailtitle => title_of($trail),
479 trailurl => urlto($trail, $page),
483 $template->param(trailloop => \@trails);