* Don't put discussion links on discussion pages.
[ikiwiki.git] / IkiWiki / Render.pm
1 #!/usr/bin/perl
2
3 package IkiWiki;
4
5 use warnings;
6 use strict;
7 use IkiWiki;
8 use Encode;
9
10 my %backlinks;
11 my $backlinks_calculated=0;
12
13 sub calculate_backlinks () { #{{{
14         return if $backlinks_calculated;
15         %backlinks=();
16         foreach my $page (keys %links) {
17                 foreach my $link (@{$links{$page}}) {
18                         my $bestlink=bestlink($page, $link);
19                         if (length $bestlink && $bestlink ne $page) {
20                                 $backlinks{$bestlink}{$page}=1;
21                         }
22                 }
23         }
24         $backlinks_calculated=1;
25 } #}}}
26
27 sub backlinks ($) { #{{{
28         my $page=shift;
29
30         calculate_backlinks();
31
32         my @links;
33         return unless $backlinks{$page};
34         foreach my $p (keys %{$backlinks{$page}}) {
35                 my $href=abs2rel(htmlpage($p), dirname($page));
36                         
37                 # Trim common dir prefixes from both pages.
38                 my $p_trimmed=$p;
39                 my $page_trimmed=$page;
40                 my $dir;
41                 1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) &&
42                         defined $dir &&
43                         $p_trimmed=~s/^\Q$dir\E// &&
44                         $page_trimmed=~s/^\Q$dir\E//;
45                                
46                 push @links, { url => $href, page => pagetitle($p_trimmed) };
47         }
48
49         return sort { $a->{page} cmp $b->{page} } @links;
50 } #}}}
51
52 sub parentlinks ($) { #{{{
53         my $page=shift;
54         
55         my @ret;
56         my $pagelink="";
57         my $path="";
58         my $skip=1;
59         return if $page eq 'index'; # toplevel
60         foreach my $dir (reverse split("/", $page)) {
61                 if (! $skip) {
62                         $path.="../";
63                         unshift @ret, { url => $path.htmlpage($dir), page => pagetitle($dir) };
64                 }
65                 else {
66                         $skip=0;
67                 }
68         }
69         unshift @ret, { url => length $path ? $path : ".", page => $config{wikiname} };
70         return @ret;
71 } #}}}
72
73 sub genpage ($$$) { #{{{
74         my $page=shift;
75         my $content=shift;
76         my $mtime=shift;
77
78         my $template=template("page.tmpl", blind_cache => 1);
79         my $actions=0;
80
81         if (length $config{cgiurl}) {
82                 $template->param(editurl => cgiurl(do => "edit", page => $page));
83                 $template->param(prefsurl => cgiurl(do => "prefs"));
84                 if ($config{rcs}) {
85                         $template->param(recentchangesurl => cgiurl(do => "recentchanges"));
86                 }
87                 $actions++;
88         }
89
90         if (length $config{historyurl}) {
91                 my $u=$config{historyurl};
92                 $u=~s/\[\[file\]\]/$pagesources{$page}/g;
93                 $template->param(historyurl => $u);
94                 $actions++;
95         }
96         if ($config{discussion} && $page !~ /.*\/discussion$/ &&
97             (length $config{cgiurl} || exists $links{$page."/".gettext("discussion")})) {
98                 $template->param(discussionlink => htmllink($page, $page, gettext("Discussion"), 1, 1));
99                 $actions++;
100         }
101
102         if ($actions) {
103                 $template->param(have_actions => 1);
104         }
105
106         $template->param(
107                 title => $page eq 'index' 
108                         ? $config{wikiname} 
109                         : pagetitle(basename($page)),
110                 wikiname => $config{wikiname},
111                 parentlinks => [parentlinks($page)],
112                 content => $content,
113                 backlinks => [backlinks($page)],
114                 mtime => displaytime($mtime),
115                 baseurl => baseurl($page),
116         );
117
118         run_hooks(pagetemplate => sub {
119                 shift->(page => $page, destpage => $page, template => $template);
120         });
121         
122         $content=$template->output;
123
124         run_hooks(format => sub {
125                 $content=shift->(
126                         page => $page,
127                         content => $content,
128                 );
129         });
130
131         return $content;
132 } #}}}
133
134 sub mtime ($) { #{{{
135         my $file=shift;
136         
137         return (stat($file))[9];
138 } #}}}
139
140 sub scan ($) { #{{{
141         my $file=shift;
142
143         my $type=pagetype($file);
144         if (defined $type) {
145                 my $srcfile=srcfile($file);
146                 my $content=readfile($srcfile);
147                 my $page=pagename($file);
148                 will_render($page, htmlpage($page), 1);
149
150                 # Always needs to be done, since filters might add links
151                 # to the content.
152                 $content=filter($page, $content);
153
154                 my @links;
155                 while ($content =~ /(?<!\\)$config{wiki_link_regexp}/g) {
156                         push @links, titlepage($2);
157                 }
158                 if ($config{discussion}) {
159                         # Discussion links are a special case since they're
160                         # not in the text of the page, but on its template.
161                         push @links, $page."/".gettext("discussion");
162                 }
163                 $links{$page}=\@links;
164                 
165                 # Preprocess in scan-only mode.
166                 preprocess($page, $page, $content, 1);
167         }
168         else {
169                 will_render($file, $file, 1);
170         }
171 } #}}}
172
173 sub render ($) { #{{{
174         my $file=shift;
175         
176         my $type=pagetype($file);
177         my $srcfile=srcfile($file);
178         if (defined $type) {
179                 my $content=readfile($srcfile);
180                 my $page=pagename($file);
181                 delete $depends{$page};
182                 will_render($page, htmlpage($page), 1);
183                 
184                 $content=filter($page, $content);
185                 $content=preprocess($page, $page, $content);
186                 $content=linkify($page, $page, $content);
187                 $content=htmlize($page, $type, $content);
188                 
189                 writefile(htmlpage($page), $config{destdir},
190                         genpage($page, $content, mtime($srcfile)));
191                 $oldpagemtime{$page}=time;
192         }
193         else {
194                 my $content=readfile($srcfile, 1);
195                 delete $depends{$file};
196                 will_render($file, $file, 1);
197                 writefile($file, $config{destdir}, $content, 1);
198                 $oldpagemtime{$file}=time;
199         }
200 } #}}}
201
202 sub prune ($) { #{{{
203         my $file=shift;
204
205         unlink($file);
206         my $dir=dirname($file);
207         while (rmdir($dir)) {
208                 $dir=dirname($dir);
209         }
210 } #}}}
211
212 sub refresh () { #{{{
213         # find existing pages
214         my %exists;
215         my @files;
216         eval q{use File::Find};
217         error($@) if $@;
218         find({
219                 no_chdir => 1,
220                 wanted => sub {
221                         $_=decode_utf8($_);
222                         if (file_pruned($_, $config{srcdir})) {
223                                 $File::Find::prune=1;
224                         }
225                         elsif (! -d $_ && ! -l $_) {
226                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
227                                 if (! defined $f) {
228                                         warn(sprintf(gettext("skipping bad filename %s"), $_)."\n");
229                                 }
230                                 else {
231                                         $f=~s/^\Q$config{srcdir}\E\/?//;
232                                         push @files, $f;
233                                         $exists{pagename($f)}=1;
234                                 }
235                         }
236                 },
237         }, $config{srcdir});
238         find({
239                 no_chdir => 1,
240                 wanted => sub {
241                         $_=decode_utf8($_);
242                         if (file_pruned($_, $config{underlaydir})) {
243                                 $File::Find::prune=1;
244                         }
245                         elsif (! -d $_ && ! -l $_) {
246                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
247                                 if (! defined $f) {
248                                         warn(sprintf(gettext("skipping bad filename %s"), $_)."\n");
249                                 }
250                                 else {
251                                         # Don't add pages that are in the
252                                         # srcdir.
253                                         $f=~s/^\Q$config{underlaydir}\E\/?//;
254                                         if (! -e "$config{srcdir}/$f" && 
255                                             ! -l "$config{srcdir}/$f") {
256                                                 my $page=pagename($f);
257                                                 if (! $exists{$page}) {
258                                                         push @files, $f;
259                                                         $exists{$page}=1;
260                                                 }
261                                         }
262                                 }
263                         }
264                 },
265         }, $config{underlaydir});
266
267         my %rendered;
268
269         # check for added or removed pages
270         my @add;
271         foreach my $file (@files) {
272                 my $page=pagename($file);
273                 $pagesources{$page}=$file;
274                 if (! $oldpagemtime{$page}) {
275                         push @add, $file;
276                         $pagecase{lc $page}=$page;
277                         if ($config{getctime} && -e "$config{srcdir}/$file") {
278                                 $pagectime{$page}=rcs_getctime("$config{srcdir}/$file");
279                         }
280                         elsif (! exists $pagectime{$page}) {
281                                 $pagectime{$page}=mtime(srcfile($file));
282                         }
283                 }
284         }
285         my @del;
286         foreach my $page (keys %oldpagemtime) {
287                 if (! $exists{$page}) {
288                         debug(sprintf(gettext("removing old page %s"), $page));
289                         push @del, $pagesources{$page};
290                         $links{$page}=[];
291                         $renderedfiles{$page}=[];
292                         $oldpagemtime{$page}=0;
293                         prune($config{destdir}."/".$_)
294                                 foreach @{$oldrenderedfiles{$page}};
295                         delete $pagesources{$page};
296                 }
297         }
298
299         # scan changed and new files
300         my @changed;
301         foreach my $file (@files) {
302                 my $page=pagename($file);
303                 
304                 if (! exists $oldpagemtime{$page} ||
305                     mtime(srcfile($file)) > $oldpagemtime{$page} ||
306                     $forcerebuild{$page}) {
307                         debug(sprintf(gettext("scanning %s"), $file));
308                         push @changed, $file;
309                         scan($file);
310                 }
311         }
312         calculate_backlinks();
313
314         # render changed and new pages
315         foreach my $file (@changed) {
316                 debug(sprintf(gettext("rendering %s"), $file));
317                 render($file);
318                 $rendered{$file}=1;
319         }
320         
321         # rebuild pages that link to added or removed pages
322         if (@add || @del) {
323                 foreach my $f (@add, @del) {
324                         my $p=pagename($f);
325                         foreach my $page (keys %{$backlinks{$p}}) {
326                                 my $file=$pagesources{$page};
327                                 next if $rendered{$file};
328                                 debug(sprintf(gettext("rendering %s, which links to %s"), $file, $p));
329                                 render($file);
330                                 $rendered{$file}=1;
331                         }
332                 }
333         }
334
335         if (%rendered || @del) {
336                 # rebuild dependant pages
337                 foreach my $f (@files) {
338                         next if $rendered{$f};
339                         my $p=pagename($f);
340                         if (exists $depends{$p}) {
341                                 foreach my $file (keys %rendered, @del) {
342                                         next if $f eq $file;
343                                         my $page=pagename($file);
344                                         if (pagespec_match($page, $depends{$p})) {
345                                                 debug(sprintf(gettext("rendering %s, which depends on %s"), $f, $page));
346                                                 render($f);
347                                                 $rendered{$f}=1;
348                                                 last;
349                                         }
350                                 }
351                         }
352                 }
353                 
354                 # handle backlinks; if a page has added/removed links,
355                 # update the pages it links to
356                 my %linkchanged;
357                 foreach my $file (keys %rendered, @del) {
358                         my $page=pagename($file);
359                         
360                         if (exists $links{$page}) {
361                                 foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) {
362                                         if (length $link &&
363                                             (! exists $oldlinks{$page} ||
364                                              ! grep { bestlink($page, $_) eq $link } @{$oldlinks{$page}})) {
365                                                 $linkchanged{$link}=1;
366                                         }
367                                 }
368                         }
369                         if (exists $oldlinks{$page}) {
370                                 foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) {
371                                         if (length $link &&
372                                             (! exists $links{$page} || 
373                                              ! grep { bestlink($page, $_) eq $link } @{$links{$page}})) {
374                                                 $linkchanged{$link}=1;
375                                         }
376                                 }
377                         }
378                 }
379                 foreach my $link (keys %linkchanged) {
380                         my $linkfile=$pagesources{$link};
381                         if (defined $linkfile) {
382                                 next if $rendered{$linkfile};
383                                 debug(sprintf(gettext("rendering %s, to update its backlinks"), $linkfile));
384                                 render($linkfile);
385                                 $rendered{$linkfile}=1;
386                         }
387                 }
388         }
389
390         # remove no longer rendered files
391         foreach my $src (keys %rendered) {
392                 my $page=pagename($src);
393                 foreach my $file (@{$oldrenderedfiles{$page}}) {
394                         if (! grep { $_ eq $file } @{$renderedfiles{$page}}) {
395                                 debug(sprintf(gettext("removing %s, no longer rendered by %s"), $file, $page));
396                                 prune($config{destdir}."/".$file);
397                         }
398                 }
399         }
400
401         if (@del) {
402                 run_hooks(delete => sub { shift->(@del) });
403         }
404         if (%rendered) {
405                 run_hooks(change => sub { shift->(keys %rendered) });
406         }
407 } #}}}
408
409 sub commandline_render () { #{{{
410         loadplugins();
411         checkconfig();
412         lockwiki();
413         loadindex();
414         unlockwiki();
415
416         my $srcfile=possibly_foolish_untaint($config{render});
417         my $file=$srcfile;
418         $file=~s/\Q$config{srcdir}\E\/?//;
419
420         my $type=pagetype($file);
421         die sprintf(gettext("ikiwiki: cannot render %s"), $srcfile)."\n" unless defined $type;
422         my $content=readfile($srcfile);
423         my $page=pagename($file);
424         $pagesources{$page}=$file;
425         $content=filter($page, $content);
426         $content=preprocess($page, $page, $content);
427         $content=linkify($page, $page, $content);
428         $content=htmlize($page, $type, $content);
429
430         print genpage($page, $content, mtime($srcfile));
431         exit 0;
432 } #}}}
433
434 1