better fix for empty actions list issue
[ikiwiki.git] / IkiWiki / Render.pm
1 #!/usr/bin/perl
2
3 package IkiWiki;
4
5 use warnings;
6 use strict;
7 use File::Spec;
8 use IkiWiki;
9
10 sub linkify ($$) { #{{{
11         my $page=shift;
12         my $content=shift;
13
14         $content =~ s{(\\?)$config{wiki_link_regexp}}{
15                 $2 ? ( $1 ? "[[$2|$3]]" : htmllink($page, titlepage($3), 0, 0, pagetitle($2)))
16                    : ( $1 ? "[[$3]]" :    htmllink($page, titlepage($3)))
17         }eg;
18         
19         return $content;
20 } #}}}
21
22 sub htmlize ($$) { #{{{
23         my $type=shift;
24         my $content=shift;
25         
26         if (! $INC{"/usr/bin/markdown"}) {
27                 no warnings 'once';
28                 $blosxom::version="is a proper perl module too much to ask?";
29                 use warnings 'all';
30                 do "/usr/bin/markdown";
31         }
32         
33         if ($type eq '.mdwn') {
34                 $content=Markdown::Markdown($content);
35         }
36         else {
37                 error("htmlization of $type not supported");
38         }
39
40         if (exists $hooks{sanitize}) {
41                 foreach my $id (keys %{$hooks{sanitize}}) {
42                         $content=$hooks{sanitize}{$id}{call}->($content);
43                 }
44         }
45         
46         return $content;
47 } #}}}
48
49 sub backlinks ($) { #{{{
50         my $page=shift;
51
52         my @links;
53         foreach my $p (keys %links) {
54                 next if bestlink($page, $p) eq $page;
55                 if (grep { length $_ && bestlink($p, $_) eq $page } @{$links{$p}}) {
56                         my $href=File::Spec->abs2rel(htmlpage($p), dirname($page));
57                         
58                         # Trim common dir prefixes from both pages.
59                         my $p_trimmed=$p;
60                         my $page_trimmed=$page;
61                         my $dir;
62                         1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) &&
63                                 defined $dir &&
64                                 $p_trimmed=~s/^\Q$dir\E// &&
65                                 $page_trimmed=~s/^\Q$dir\E//;
66                                        
67                         push @links, { url => $href, page => $p_trimmed };
68                 }
69         }
70
71         return sort { $a->{page} cmp $b->{page} } @links;
72 } #}}}
73
74 sub parentlinks ($) { #{{{
75         my $page=shift;
76         
77         my @ret;
78         my $pagelink="";
79         my $path="";
80         my $skip=1;
81         foreach my $dir (reverse split("/", $page)) {
82                 if (! $skip) {
83                         $path.="../";
84                         unshift @ret, { url => "$path$dir.html", page => $dir };
85                 }
86                 else {
87                         $skip=0;
88                 }
89         }
90         unshift @ret, { url => length $path ? $path : ".", page => $config{wikiname} };
91         return @ret;
92 } #}}}
93
94 sub preprocess ($$) { #{{{
95         my $page=shift;
96         my $content=shift;
97
98         my $handle=sub {
99                 my $escape=shift;
100                 my $command=shift;
101                 my $params=shift;
102                 if (length $escape) {
103                         return "[[$command $params]]";
104                 }
105                 elsif (exists $hooks{preprocess}{$command}) {
106                         my %params;
107                         while ($params =~ /(\w+)=\"([^"]+)"(\s+|$)/g) {
108                                 $params{$1}=$2;
109                         }
110                         return $hooks{preprocess}{$command}{call}->(page => $page, %params);
111                 }
112                 else {
113                         return "[[$command not processed]]";
114                 }
115         };
116         
117         $content =~ s{(\\?)$config{wiki_processor_regexp}}{$handle->($1, $2, $3)}eg;
118         return $content;
119 } #}}}
120
121 sub add_depends ($$) { #{{{
122         my $page=shift;
123         my $globlist=shift;
124         
125         if (! exists $depends{$page}) {
126                 $depends{$page}=$globlist;
127         }
128         else {
129                 $depends{$page}=globlist_merge($depends{$page}, $globlist);
130         }
131 } # }}}
132
133 sub globlist_merge ($$) { #{{{
134         my $a=shift;
135         my $b=shift;
136
137         my $ret="";
138         # Only add negated globs if they are not matched by the other globlist.
139         foreach my $i ((map { [ $a, $_ ] } split(" ", $b)), 
140                        (map { [ $b, $_ ] } split(" ", $a))) {
141                 if ($i->[1]=~/^!(.*)/) {
142                         if (! globlist_match($1, $i->[0])) {
143                                 $ret.=" ".$i->[1];
144                         }
145                 }
146                 else {
147                         $ret.=" ".$i->[1];
148                 }
149         }
150         
151         return $ret;
152 } #}}}
153
154 sub genpage ($$$) { #{{{
155         my $page=shift;
156         my $content=shift;
157         my $mtime=shift;
158
159         my $title=pagetitle(basename($page));
160         
161         my $template=HTML::Template->new(blind_cache => 1,
162                 filename => "$config{templatedir}/page.tmpl");
163         my $actions=0;
164
165         if (length $config{cgiurl}) {
166                 $template->param(editurl => cgiurl(do => "edit", page => $page));
167                 $template->param(prefsurl => cgiurl(do => "prefs"));
168                 if ($config{rcs}) {
169                         $template->param(recentchangesurl => cgiurl(do => "recentchanges"));
170                 }
171                 $actions++;
172         }
173
174         if (length $config{historyurl}) {
175                 my $u=$config{historyurl};
176                 $u=~s/\[\[file\]\]/$pagesources{$page}/g;
177                 $template->param(historyurl => $u);
178                 $actions++;
179         }
180         if ($config{discussion}) {
181                 $template->param(discussionlink => htmllink($page, "Discussion", 1, 1));
182                 $actions++;
183         }
184
185         if ($actions) {
186                 $template->param(have_actions => 1);
187         }
188
189         if (exists $hooks{pagetemplate}) {
190                 foreach my $id (keys %{$hooks{pagetemplate}}) {
191                         $hooks{pagetemplate}{$id}{call}->($page, $template);
192                 }
193         }
194
195         $template->param(
196                 title => $title,
197                 wikiname => $config{wikiname},
198                 parentlinks => [parentlinks($page)],
199                 content => $content,
200                 backlinks => [backlinks($page)],
201                 mtime => scalar(gmtime($mtime)),
202                 styleurl => styleurl($page),
203         );
204         
205         return $template->output;
206 } #}}}
207
208 sub check_overwrite ($$) { #{{{
209         # Important security check. Make sure to call this before saving
210         # any files to the source directory.
211         my $dest=shift;
212         my $src=shift;
213         
214         if (! exists $renderedfiles{$src} && -e $dest && ! $config{rebuild}) {
215                 error("$dest already exists and was rendered from ".
216                         join(" ",(grep { $renderedfiles{$_} eq $dest } keys
217                                 %renderedfiles)).
218                         ", before, so not rendering from $src");
219         }
220 } #}}}
221
222 sub mtime ($) { #{{{
223         my $file=shift;
224         
225         return (stat($file))[9];
226 } #}}}
227
228 sub findlinks ($$) { #{{{
229         my $page=shift;
230         my $content=shift;
231
232         my @links;
233         while ($content =~ /(?<!\\)$config{wiki_link_regexp}/g) {
234                 push @links, titlepage($2);
235         }
236         if ($config{discussion}) {
237                 # Discussion links are a special case since they're not in the
238                 # text of the page, but on its template.
239                 return @links, "$page/discussion";
240         }
241         else {
242                 return @links;
243         }
244 } #}}}
245
246 sub render ($) { #{{{
247         my $file=shift;
248         
249         my $type=pagetype($file);
250         my $srcfile=srcfile($file);
251         if ($type ne 'unknown') {
252                 my $content=readfile($srcfile);
253                 my $page=pagename($file);
254                 delete $depends{$page};
255                 
256                 if (exists $hooks{filter}) {
257                         foreach my $id (keys %{$hooks{filter}}) {
258                                 $content=$hooks{filter}{$id}{call}->(
259                                         page => $page,
260                                         content => $content
261                                 );
262                         }
263                 }
264                 
265                 $links{$page}=[findlinks($page, $content)];
266                 
267                 $content=linkify($page, $content);
268                 $content=preprocess($page, $content);
269                 $content=htmlize($type, $content);
270                 
271                 check_overwrite("$config{destdir}/".htmlpage($page), $page);
272                 writefile(htmlpage($page), $config{destdir},
273                         genpage($page, $content, mtime($srcfile)));
274                 $oldpagemtime{$page}=time;
275                 $renderedfiles{$page}=htmlpage($page);
276         }
277         else {
278                 my $content=readfile($srcfile, 1);
279                 $links{$file}=[];
280                 delete $depends{$file};
281                 check_overwrite("$config{destdir}/$file", $file);
282                 writefile($file, $config{destdir}, $content, 1);
283                 $oldpagemtime{$file}=time;
284                 $renderedfiles{$file}=$file;
285         }
286 } #}}}
287
288 sub prune ($) { #{{{
289         my $file=shift;
290
291         unlink($file);
292         my $dir=dirname($file);
293         while (rmdir($dir)) {
294                 $dir=dirname($dir);
295         }
296 } #}}}
297
298 sub refresh () { #{{{
299         # find existing pages
300         my %exists;
301         my @files;
302         eval q{use File::Find};
303         find({
304                 no_chdir => 1,
305                 wanted => sub {
306                         if (/$config{wiki_file_prune_regexp}/) {
307                                 $File::Find::prune=1;
308                         }
309                         elsif (! -d $_ && ! -l $_) {
310                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
311                                 if (! defined $f) {
312                                         warn("skipping bad filename $_\n");
313                                 }
314                                 else {
315                                         $f=~s/^\Q$config{srcdir}\E\/?//;
316                                         push @files, $f;
317                                         $exists{pagename($f)}=1;
318                                 }
319                         }
320                 },
321         }, $config{srcdir});
322         find({
323                 no_chdir => 1,
324                 wanted => sub {
325                         if (/$config{wiki_file_prune_regexp}/) {
326                                 $File::Find::prune=1;
327                         }
328                         elsif (! -d $_ && ! -l $_) {
329                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
330                                 if (! defined $f) {
331                                         warn("skipping bad filename $_\n");
332                                 }
333                                 else {
334                                         # Don't add files that are in the
335                                         # srcdir.
336                                         $f=~s/^\Q$config{underlaydir}\E\/?//;
337                                         if (! -e "$config{srcdir}/$f" && 
338                                             ! -l "$config{srcdir}/$f") {
339                                                 push @files, $f;
340                                                 $exists{pagename($f)}=1;
341                                         }
342                                 }
343                         }
344                 },
345         }, $config{underlaydir});
346
347         my %rendered;
348
349         # check for added or removed pages
350         my @add;
351         foreach my $file (@files) {
352                 my $page=pagename($file);
353                 if (! $oldpagemtime{$page}) {
354                         debug("new page $page") unless exists $pagectime{$page};
355                         push @add, $file;
356                         $links{$page}=[];
357                         $pagesources{$page}=$file;
358                         if ($config{getctime} && -e "$config{srcdir}/$file") {
359                                 $pagectime{$page}=rcs_getctime("$config{srcdir}/$file");
360                         }
361                         elsif (! exists $pagectime{$page}) {
362                                 $pagectime{$page}=mtime(srcfile($file));
363                         }
364                 }
365         }
366         my @del;
367         foreach my $page (keys %oldpagemtime) {
368                 if (! $exists{$page}) {
369                         debug("removing old page $page");
370                         push @del, $pagesources{$page};
371                         prune($config{destdir}."/".$renderedfiles{$page});
372                         delete $renderedfiles{$page};
373                         $oldpagemtime{$page}=0;
374                         delete $pagesources{$page};
375                 }
376         }
377         
378         # render any updated files
379         foreach my $file (@files) {
380                 my $page=pagename($file);
381                 
382                 if (! exists $oldpagemtime{$page} ||
383                     mtime(srcfile($file)) > $oldpagemtime{$page}) {
384                         debug("rendering changed file $file");
385                         render($file);
386                         $rendered{$file}=1;
387                 }
388         }
389         
390         # if any files were added or removed, check to see if each page
391         # needs an update due to linking to them or inlining them.
392         # TODO: inefficient; pages may get rendered above and again here;
393         # problem is the bestlink may have changed and we won't know until
394         # now
395         if (@add || @del) {
396 FILE:           foreach my $file (@files) {
397                         my $page=pagename($file);
398                         foreach my $f (@add, @del) {
399                                 my $p=pagename($f);
400                                 foreach my $link (@{$links{$page}}) {
401                                         if (bestlink($page, $link) eq $p) {
402                                                 debug("rendering $file, which links to $p");
403                                                 render($file);
404                                                 $rendered{$file}=1;
405                                                 next FILE;
406                                         }
407                                 }
408                         }
409                 }
410         }
411
412         # Handle backlinks; if a page has added/removed links, update the
413         # pages it links to. Also handles rebuilding dependat pages.
414         # TODO: inefficient; pages may get rendered above and again here;
415         # problem is the backlinks could be wrong in the first pass render
416         # above
417         if (%rendered || @del) {
418                 foreach my $f (@files) {
419                         my $p=pagename($f);
420                         if (exists $depends{$p}) {
421                                 foreach my $file (keys %rendered, @del) {
422                                         next if $f eq $file;
423                                         my $page=pagename($file);
424                                         if (globlist_match($page, $depends{$p})) {
425                                                 debug("rendering $f, which depends on $page");
426                                                 render($f);
427                                                 $rendered{$f}=1;
428                                                 last;
429                                         }
430                                 }
431                         }
432                 }
433                 
434                 my %linkchanged;
435                 foreach my $file (keys %rendered, @del) {
436                         my $page=pagename($file);
437                         
438                         if (exists $links{$page}) {
439                                 foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) {
440                                         if (length $link &&
441                                             (! exists $oldlinks{$page} ||
442                                              ! grep { bestlink($page, $_) eq $link } @{$oldlinks{$page}})) {
443                                                 $linkchanged{$link}=1;
444                                         }
445                                 }
446                         }
447                         if (exists $oldlinks{$page}) {
448                                 foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) {
449                                         if (length $link &&
450                                             (! exists $links{$page} || 
451                                              ! grep { bestlink($page, $_) eq $link } @{$links{$page}})) {
452                                                 $linkchanged{$link}=1;
453                                         }
454                                 }
455                         }
456                 }
457                 foreach my $link (keys %linkchanged) {
458                         my $linkfile=$pagesources{$link};
459                         if (defined $linkfile) {
460                                 debug("rendering $linkfile, to update its backlinks");
461                                 render($linkfile);
462                                 $rendered{$linkfile}=1;
463                         }
464                 }
465         }
466
467         if (@del && exists $hooks{delete}) {
468                 foreach my $id (keys %{$hooks{delete}}) {
469                         $hooks{delete}{$id}{call}->(@del);
470                 }
471         }
472         if (%rendered && exists $hooks{change}) {
473                 foreach my $id (keys %{$hooks{change}}) {
474                         $hooks{change}{$id}{call}->(keys %rendered);
475                 }
476         }
477 } #}}}
478
479 1