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 => "trailoptions", call => \&preprocess_trailoptions, 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 => "trailitems", call => \&preprocess_trailitems, scan => 1);
19 hook(type => "preprocess", id => "traillink", call => \&preprocess_traillink, scan => 1);
20 hook(type => "pagetemplate", id => "trail", call => \&pagetemplate);
25 If a page C<$T> is a trail, then it can have
29 =item * C<$pagestate{$T}{trail}{contents}>
31 Reference to an array of lists each containing either:
35 =item * C<[link, "link"]>
37 A link specification, pointing to the same page that C<[[link]]> would select
39 =item * C<[pagespec, "posts/*", "age", 0]>
41 A match by pagespec; the third array element is the sort order and the fourth
42 is whether to reverse sorting
46 =item * C<$pagestate{$T}{trail}{sort}>
48 A [[ikiwiki/pagespec/sorting]] order; if absent or undef, the trail is in
49 the order given by the links that form it
51 =item * C<$pagestate{$T}{trail}{circular}>
53 True if this trail is circular (i.e. going "next" from the last item is
54 allowed, and takes you back to the first)
56 =item * C<$pagestate{$T}{trail}{reverse}>
58 True if C<sort> is to be reversed.
62 If a page C<$M> is a member of a trail C<$T>, then it has
66 =item * C<$pagestate{$M}{trail}{item}{$T}[0]>
68 The page before this one in C<$T> at the last rebuild, or undef.
70 =item * C<$pagestate{$M}{trail}{item}{$T}[1]>
72 The page after this one in C<$T> at the last refresh, or undef.
88 foreach my $page (keys %pagestate) {
89 if (exists $pagestate{$page}{trail}) {
90 if (exists $pagesources{$page} &&
91 grep { $_ eq $pagesources{$page} } @$needsbuild) {
92 # Remove state, it will be re-added
93 # if the preprocessor directive is still
94 # there during the rebuild. {item} is the
95 # only thing that's added for items, not
96 # trails, and it's harmless to delete that -
97 # the item is being rebuilt anyway.
98 delete $pagestate{$page}{trail};
107 sub preprocess_trailoptions (@) {
110 if (exists $params{circular}) {
111 $pagestate{$params{page}}{trail}{circular} =
112 IkiWiki::yesno($params{circular});
115 if (exists $params{sort}) {
116 $pagestate{$params{page}}{trail}{sort} = $params{sort};
119 if (exists $params{reverse}) {
120 $pagestate{$params{page}}{trail}{reverse} = $params{reverse};
126 sub preprocess_trailinline (@) {
129 if (! exists $params{sort}) {
130 # sort in the same order as [[plugins/inline]]'s default
131 $params{sort} = 'age';
134 if (defined wantarray) {
135 scalar preprocess_trailitems(%params);
137 if (IkiWiki->can("preprocess_inline")) {
138 return IkiWiki::preprocess_inline(@_);
141 error("trailinline directive requires the inline plugin");
145 preprocess_trailitems(%params);
149 sub preprocess_trailitem (@) {
153 # avoid collecting everything in the preprocess stage if we already
154 # did in the scan stage
155 if (defined wantarray) {
156 return "" if $scanned;
163 my $trail = $params{page};
165 $link = linkpage($link);
167 add_link($params{page}, $link, 'trail');
168 push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link];
173 sub preprocess_trailitems (@) {
176 # avoid collecting everything in the preprocess stage if we already
177 # did in the scan stage
178 if (defined wantarray) {
179 return "" if $scanned;
185 # trail members from a pagespec ought to be in some sort of order,
186 # and path is a nice obvious default
187 $params{sort} = 'path' unless exists $params{sort};
188 $params{reverse} = 'no' unless exists $params{reverse};
190 if (exists $params{pages}) {
191 push @{$pagestate{$params{page}}{trail}{contents}},
192 ["pagespec" => $params{pages}, $params{sort},
193 IkiWiki::yesno($params{reverse})];
196 if (exists $params{pagenames}) {
197 my @list = map { [link => $_] } split ' ', $params{pagenames};
198 push @{$pagestate{$params{page}}{trail}{contents}}, @list;
204 sub preprocess_traillink (@) {
209 my $trail = $params{page};
213 ([^\|]+) # 1: link text
217 (.+) # 2: page to link to
221 $link = linkpage($2);
223 add_link($params{page}, $link, 'trail');
225 # avoid collecting everything in the preprocess stage if we already
226 # did in the scan stage
228 if (defined wantarray) {
235 push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link] unless $already;
237 if (defined $linktext) {
238 $linktext = pagetitle($linktext);
241 if (exists $params{text}) {
242 $linktext = $params{text};
245 if (defined $linktext) {
246 return htmllink($trail, $params{destpage},
247 $link, linktext => $linktext);
250 return htmllink($trail, $params{destpage}, $link);
253 # trail => [member1, member2]
254 my %trail_to_members;
255 # member => { trail => [prev, next] }
256 # e.g. if %trail_to_members = (
257 # trail1 => ["member1", "member2"],
258 # trail2 => ["member0", "member1"],
261 # then $member_to_trails{member1} = {
262 # trail1 => [undef, "member2"],
263 # trail2 => ["member0", undef],
265 my %member_to_trails;
268 my %rebuild_trail_members;
271 my ($old, $new) = @_;
273 foreach my $trail (keys %$old) {
274 if (! exists $new->{$trail}) {
277 my ($old_p, $old_n) = @{$old->{$trail}};
278 my ($new_p, $new_n) = @{$new->{$trail}};
279 $old_p = "" unless defined $old_p;
280 $old_n = "" unless defined $old_n;
281 $new_p = "" unless defined $new_p;
282 $new_n = "" unless defined $new_n;
283 if ($old_p ne $new_p) {
286 if ($old_n ne $new_n) {
291 foreach my $trail (keys %$new) {
292 if (! exists $old->{$trail}) {
300 my $done_prerender = 0;
305 return if $done_prerender;
307 $origsubs{render_backlinks} = \&IkiWiki::render_backlinks;
308 inject(name => "IkiWiki::render_backlinks", call => \&render_backlinks);
310 %trail_to_members = ();
311 %member_to_trails = ();
313 foreach my $trail (keys %pagestate) {
314 next unless exists $pagestate{$trail}{trail}{contents};
317 my @contents = @{$pagestate{$trail}{trail}{contents}};
319 foreach my $c (@contents) {
320 if ($c->[0] eq 'pagespec') {
321 push @$members, pagespec_match_list($trail,
322 $c->[1], sort => $c->[2],
325 elsif ($c->[0] eq 'link') {
326 my $best = bestlink($trail, $c->[1]);
327 push @$members, $best if length $best;
331 if (defined $pagestate{$trail}{trail}{sort}) {
333 @$members = pagespec_match_list($trail, 'internal(*)',
335 sort => $pagestate{$trail}{trail}{sort});
338 if (IkiWiki::yesno $pagestate{$trail}{trail}{reverse}) {
339 @$members = reverse @$members;
345 foreach my $member (@$members) {
346 push @tmp, $member unless $seen{$member};
351 for (my $i = 0; $i <= $#$members; $i++) {
352 my $member = $members->[$i];
354 $prev = $members->[$i - 1] if $i > 0;
355 my $next = $members->[$i + 1];
357 add_depends($member, $trail);
359 $member_to_trails{$member}{$trail} = [$prev, $next];
362 if ((scalar @$members) > 1 && $pagestate{$trail}{trail}{circular}) {
363 $member_to_trails{$members->[0]}{$trail}[0] = $members->[$#$members];
364 $member_to_trails{$members->[$#$members]}{$trail}[1] = $members->[0];
367 $trail_to_members{$trail} = $members;
370 foreach my $member (keys %pagestate) {
371 if (exists $pagestate{$member}{trail}{item} &&
372 ! exists $member_to_trails{$member}) {
373 $rebuild_trail_members{$member} = 1;
374 delete $pagestate{$member}{trailitem};
378 foreach my $member (keys %member_to_trails) {
379 if (! exists $pagestate{$member}{trail}{item}) {
380 $rebuild_trail_members{$member} = 1;
383 if (trails_differ($pagestate{$member}{trail}{item},
384 $member_to_trails{$member})) {
385 $rebuild_trail_members{$member} = 1;
389 $pagestate{$member}{trail}{item} = $member_to_trails{$member};
395 # This is called at about the right time that we can hijack it to render
397 sub render_backlinks ($) {
400 foreach my $member (keys %rebuild_trail_members) {
401 next unless exists $pagesources{$member};
403 IkiWiki::render($pagesources{$member}, sprintf(gettext("building %s, its previous or next page has changed"), $member));
406 $origsubs{render_backlinks}($blc);
411 if (defined ($pagestate{$page}{meta}{title})) {
412 return $pagestate{$page}{meta}{title};
414 return pagetitle(IkiWiki::basename($page));
419 sub pagetemplate (@) {
421 my $page = $params{page};
422 my $template = $params{template};
424 if ($template->query(name => 'trails') && ! $recursive) {
428 my $inner = template("trails.tmpl", blind_cache => 1);
429 IkiWiki::run_hooks(pagetemplate => sub {
430 shift->(%params, template => $inner)
432 $template->param(trails => $inner->output);
436 if ($template->query(name => 'trailloop')) {
441 # sort backlinks by page name to have a consistent order
442 foreach my $trail (sort keys %{$member_to_trails{$page}}) {
444 my $members = $trail_to_members{$trail};
445 my ($prev, $next) = @{$member_to_trails{$page}{$trail}};
446 my ($prevurl, $nexturl, $prevtitle, $nexttitle);
449 add_depends($params{destpage}, $prev);
450 $prevurl = urlto($prev, $page);
451 $prevtitle = title_of($prev);
455 add_depends($params{destpage}, $next);
456 $nexturl = urlto($next, $page);
457 $nexttitle = title_of($next);
462 prevtitle => $prevtitle,
465 nexttitle => $nexttitle,
468 trailtitle => title_of($trail),
469 trailurl => urlto($trail, $page),
473 $template->param(trailloop => \@trails);