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