foo
[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}) {
97                 $template->param(discussionlink => htmllink($page, $page, "Discussion", 1, 1));
98                 $actions++;
99         }
100
101         if ($actions) {
102                 $template->param(have_actions => 1);
103         }
104
105         $template->param(
106                 title => $page eq 'index' 
107                         ? $config{wikiname} 
108                         : pagetitle(basename($page)),
109                 wikiname => $config{wikiname},
110                 parentlinks => [parentlinks($page)],
111                 content => $content,
112                 backlinks => [backlinks($page)],
113                 mtime => displaytime($mtime),
114                 baseurl => baseurl($page),
115         );
116
117         run_hooks(pagetemplate => sub {
118                 shift->(page => $page, destpage => $page, template => $template);
119         });
120         
121         $content=$template->output;
122
123         run_hooks(format => sub {
124                 $content=shift->(
125                         page => $page,
126                         content => $content,
127                 );
128         });
129
130         return $content;
131 } #}}}
132
133 sub mtime ($) { #{{{
134         my $file=shift;
135         
136         return (stat($file))[9];
137 } #}}}
138
139 sub scan ($) { #{{{
140         my $file=shift;
141
142         my $type=pagetype($file);
143         if (defined $type) {
144                 my $srcfile=srcfile($file);
145                 my $content=readfile($srcfile);
146                 my $page=pagename($file);
147                 will_render($page, htmlpage($page), 1);
148
149                 # Always needs to be done, since filters might add links
150                 # to the content.
151                 $content=filter($page, $content);
152
153                 my @links;
154                 while ($content =~ /(?<!\\)$config{wiki_link_regexp}/g) {
155                         push @links, titlepage($2);
156                 }
157                 if ($config{discussion}) {
158                         # Discussion links are a special case since they're
159                         # not in the text of the page, but on its template.
160                         push @links, "$page/discussion";
161                 }
162                 $links{$page}=\@links;
163                 
164                 # Preprocess in scan-only mode.
165                 preprocess($page, $page, $content, 1);
166         }
167         else {
168                 will_render($file, $file, 1);
169         }
170 } #}}}
171
172 sub render ($) { #{{{
173         my $file=shift;
174         
175         my $type=pagetype($file);
176         my $srcfile=srcfile($file);
177         if (defined $type) {
178                 my $content=readfile($srcfile);
179                 my $page=pagename($file);
180                 delete $depends{$page};
181                 will_render($page, htmlpage($page), 1);
182                 
183                 $content=filter($page, $content);
184                 $content=preprocess($page, $page, $content);
185                 $content=linkify($page, $page, $content);
186                 $content=htmlize($page, $type, $content);
187                 
188                 writefile(htmlpage($page), $config{destdir},
189                         genpage($page, $content, mtime($srcfile)));
190                 $oldpagemtime{$page}=time;
191         }
192         else {
193                 my $content=readfile($srcfile, 1);
194                 delete $depends{$file};
195                 will_render($file, $file, 1);
196                 writefile($file, $config{destdir}, $content, 1);
197                 $oldpagemtime{$file}=time;
198         }
199 } #}}}
200
201 sub prune ($) { #{{{
202         my $file=shift;
203
204         unlink($file);
205         my $dir=dirname($file);
206         while (rmdir($dir)) {
207                 $dir=dirname($dir);
208         }
209 } #}}}
210
211 sub refresh () { #{{{
212         # find existing pages
213         my %exists;
214         my @files;
215         eval q{use File::Find};
216         error($@) if $@;
217         find({
218                 no_chdir => 1,
219                 wanted => sub {
220                         $_=decode_utf8($_);
221                         if (/$config{wiki_file_prune_regexp}/) {
222                                 $File::Find::prune=1;
223                         }
224                         elsif (! -d $_ && ! -l $_) {
225                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
226                                 if (! defined $f) {
227                                         warn("skipping bad filename $_\n");
228                                 }
229                                 else {
230                                         $f=~s/^\Q$config{srcdir}\E\/?//;
231                                         push @files, $f;
232                                         $exists{pagename($f)}=1;
233                                 }
234                         }
235                 },
236         }, $config{srcdir});
237         find({
238                 no_chdir => 1,
239                 wanted => sub {
240                         $_=decode_utf8($_);
241                         if (/$config{wiki_file_prune_regexp}/) {
242                                 $File::Find::prune=1;
243                         }
244                         elsif (! -d $_ && ! -l $_) {
245                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
246                                 if (! defined $f) {
247                                         warn("skipping bad filename $_\n");
248                                 }
249                                 else {
250                                         # Don't add files that are in the
251                                         # srcdir.
252                                         $f=~s/^\Q$config{underlaydir}\E\/?//;
253                                         if (! -e "$config{srcdir}/$f" && 
254                                             ! -l "$config{srcdir}/$f") {
255                                                 push @files, $f;
256                                                 $exists{pagename($f)}=1;
257                                         }
258                                 }
259                         }
260                 },
261         }, $config{underlaydir});
262
263         my %rendered;
264
265         # check for added or removed pages
266         my @add;
267         foreach my $file (@files) {
268                 my $page=pagename($file);
269                 if (! $oldpagemtime{$page}) {
270                         push @add, $file;
271                         $pagecase{lc $page}=$page;
272                         $pagesources{$page}=$file;
273                         if ($config{getctime} && -e "$config{srcdir}/$file") {
274                                 $pagectime{$page}=rcs_getctime("$config{srcdir}/$file");
275                         }
276                         elsif (! exists $pagectime{$page}) {
277                                 $pagectime{$page}=mtime(srcfile($file));
278                         }
279                 }
280         }
281         my @del;
282         foreach my $page (keys %oldpagemtime) {
283                 if (! $exists{$page}) {
284                         debug("removing old page $page");
285                         push @del, $pagesources{$page};
286                         $links{$page}=[];
287                         $renderedfiles{$page}=[];
288                         $oldpagemtime{$page}=0;
289                         prune($config{destdir}."/".$_)
290                                 foreach @{$oldrenderedfiles{$page}};
291                         delete $pagesources{$page};
292                 }
293         }
294
295         # scan changed and new files
296         my @changed;
297         foreach my $file (@files) {
298                 my $page=pagename($file);
299                 
300                 if (! exists $oldpagemtime{$page} ||
301                     mtime(srcfile($file)) > $oldpagemtime{$page} ||
302                     $forcerebuild{$page}) {
303                         debug("scanning $file");
304                         push @changed, $file;
305                         scan($file);
306                 }
307         }
308         calculate_backlinks();
309
310         # render changed and new pages
311         foreach my $file (@changed) {
312                 debug("rendering $file");
313                 render($file);
314                 $rendered{$file}=1;
315         }
316         
317         # rebuild pages that link to added or removed pages
318         if (@add || @del) {
319                 foreach my $f (@add, @del) {
320                         my $p=pagename($f);
321                         foreach my $page (keys %{$backlinks{$p}}) {
322                                 my $file=$pagesources{$page};
323                                 next if $rendered{$file};
324                                 debug("rendering $file, which links to $p");
325                                 render($file);
326                                 $rendered{$file}=1;
327                         }
328                 }
329         }
330
331         if (%rendered || @del) {
332                 # rebuild dependant pages
333                 foreach my $f (@files) {
334                         next if $rendered{$f};
335                         my $p=pagename($f);
336                         if (exists $depends{$p}) {
337                                 foreach my $file (keys %rendered, @del) {
338                                         next if $f eq $file;
339                                         my $page=pagename($file);
340                                         if (pagespec_match($page, $depends{$p})) {
341                                                 debug("rendering $f, which depends on $page");
342                                                 render($f);
343                                                 $rendered{$f}=1;
344                                                 last;
345                                         }
346                                 }
347                         }
348                 }
349                 
350                 # handle backlinks; if a page has added/removed links,
351                 # update the pages it links to
352                 my %linkchanged;
353                 foreach my $file (keys %rendered, @del) {
354                         my $page=pagename($file);
355                         
356                         if (exists $links{$page}) {
357                                 foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) {
358                                         if (length $link &&
359                                             (! exists $oldlinks{$page} ||
360                                              ! grep { bestlink($page, $_) eq $link } @{$oldlinks{$page}})) {
361                                                 $linkchanged{$link}=1;
362                                         }
363                                 }
364                         }
365                         if (exists $oldlinks{$page}) {
366                                 foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) {
367                                         if (length $link &&
368                                             (! exists $links{$page} || 
369                                              ! grep { bestlink($page, $_) eq $link } @{$links{$page}})) {
370                                                 $linkchanged{$link}=1;
371                                         }
372                                 }
373                         }
374                 }
375                 foreach my $link (keys %linkchanged) {
376                         my $linkfile=$pagesources{$link};
377                         if (defined $linkfile) {
378                                 next if $rendered{$linkfile};
379                                 debug("rendering $linkfile, to update its backlinks");
380                                 render($linkfile);
381                                 $rendered{$linkfile}=1;
382                         }
383                 }
384         }
385
386         # remove no longer rendered files
387         foreach my $src (keys %rendered) {
388                 my $page=pagename($src);
389                 foreach my $file (@{$oldrenderedfiles{$page}}) {
390                         if (! grep { $_ eq $file } @{$renderedfiles{$page}}) {
391                                 debug("removing $file, no longer rendered by $page");
392                                 prune($config{destdir}."/".$file);
393                         }
394                 }
395         }
396
397         if (@del) {
398                 run_hooks(delete => sub { shift->(@del) });
399         }
400         if (%rendered) {
401                 run_hooks(change => sub { shift->(keys %rendered) });
402         }
403 } #}}}
404
405 sub commandline_render () { #{{{
406         loadplugins();
407         checkconfig();
408         lockwiki();
409         loadindex();
410         unlockwiki();
411
412         my $srcfile=possibly_foolish_untaint($config{render});
413         my $file=$srcfile;
414         $file=~s/\Q$config{srcdir}\E\/?//;
415
416         my $type=pagetype($file);
417         die "ikiwiki: cannot render $srcfile\n" unless defined $type;
418         my $content=readfile($srcfile);
419         my $page=pagename($file);
420         $pagesources{$page}=$file;
421         $content=filter($page, $content);
422         $content=preprocess($page, $page, $content);
423         $content=linkify($page, $page, $content);
424         $content=htmlize($page, $type, $content);
425
426         print genpage($page, $content, mtime($srcfile));
427         exit 0;
428 } #}}}
429
430 1