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