* Removed --sanitize and --no-sanitize, replaced with --plugin htmlscrubber
[ikiwiki] / 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 $content=shift;
12         my $page=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 $content=shift;
156         my $page=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         
164         if (length $config{cgiurl}) {
165                 $template->param(editurl => cgiurl(do => "edit", page => $page));
166                 $template->param(prefsurl => cgiurl(do => "prefs"));
167                 if ($config{rcs}) {
168                         $template->param(recentchangesurl => cgiurl(do => "recentchanges"));
169                 }
170         }
171
172         if (length $config{historyurl}) {
173                 my $u=$config{historyurl};
174                 $u=~s/\[\[file\]\]/$pagesources{$page}/g;
175                 $template->param(historyurl => $u);
176         }
177         $template->param(headercontent => $config{headercontent});
178
179         $template->param(
180                 title => $title,
181                 wikiname => $config{wikiname},
182                 parentlinks => [parentlinks($page)],
183                 content => $content,
184                 backlinks => [backlinks($page)],
185                 discussionlink => htmllink($page, "Discussion", 1, 1),
186                 mtime => scalar(gmtime($mtime)),
187                 styleurl => styleurl($page),
188         );
189         
190         return $template->output;
191 } #}}}
192
193 sub check_overwrite ($$) { #{{{
194         # Important security check. Make sure to call this before saving
195         # any files to the source directory.
196         my $dest=shift;
197         my $src=shift;
198         
199         if (! exists $renderedfiles{$src} && -e $dest && ! $config{rebuild}) {
200                 error("$dest already exists and was rendered from ".
201                         join(" ",(grep { $renderedfiles{$_} eq $dest } keys
202                                 %renderedfiles)).
203                         ", before, so not rendering from $src");
204         }
205 } #}}}
206
207 sub mtime ($) { #{{{
208         my $file=shift;
209         
210         return (stat($file))[9];
211 } #}}}
212
213 sub findlinks ($$) { #{{{
214         my $content=shift;
215         my $page=shift;
216
217         my @links;
218         while ($content =~ /(?<!\\)$config{wiki_link_regexp}/g) {
219                 push @links, titlepage($2);
220         }
221         # Discussion links are a special case since they're not in the text
222         # of the page, but on its template.
223         return @links, "$page/discussion";
224 } #}}}
225
226 sub render ($) { #{{{
227         my $file=shift;
228         
229         my $type=pagetype($file);
230         my $srcfile=srcfile($file);
231         if ($type ne 'unknown') {
232                 my $content=readfile($srcfile);
233                 my $page=pagename($file);
234                 delete $depends{$page};
235                 
236                 if (exists $hooks{filter}) {
237                         foreach my $id (keys %{$hooks{filter}}) {
238                                 $content=$hooks{filter}{$id}{call}->(
239                                         page => $page,
240                                         content => $content
241                                 );
242                         }
243                 }
244                 
245                 $links{$page}=[findlinks($content, $page)];
246                 
247                 $content=linkify($content, $page);
248                 $content=preprocess($page, $content);
249                 $content=htmlize($type, $content);
250                 
251                 check_overwrite("$config{destdir}/".htmlpage($page), $page);
252                 writefile(htmlpage($page), $config{destdir},
253                         genpage($content, $page, mtime($srcfile)));
254                 $oldpagemtime{$page}=time;
255                 $renderedfiles{$page}=htmlpage($page);
256         }
257         else {
258                 my $content=readfile($srcfile, 1);
259                 $links{$file}=[];
260                 delete $depends{$file};
261                 check_overwrite("$config{destdir}/$file", $file);
262                 writefile($file, $config{destdir}, $content, 1);
263                 $oldpagemtime{$file}=time;
264                 $renderedfiles{$file}=$file;
265         }
266 } #}}}
267
268 sub prune ($) { #{{{
269         my $file=shift;
270
271         unlink($file);
272         my $dir=dirname($file);
273         while (rmdir($dir)) {
274                 $dir=dirname($dir);
275         }
276 } #}}}
277
278 sub refresh () { #{{{
279         # find existing pages
280         my %exists;
281         my @files;
282         eval q{use File::Find};
283         find({
284                 no_chdir => 1,
285                 wanted => sub {
286                         if (/$config{wiki_file_prune_regexp}/) {
287                                 $File::Find::prune=1;
288                         }
289                         elsif (! -d $_ && ! -l $_) {
290                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
291                                 if (! defined $f) {
292                                         warn("skipping bad filename $_\n");
293                                 }
294                                 else {
295                                         $f=~s/^\Q$config{srcdir}\E\/?//;
296                                         push @files, $f;
297                                         $exists{pagename($f)}=1;
298                                 }
299                         }
300                 },
301         }, $config{srcdir});
302         find({
303                 no_chdir => 1,
304                 wanted => sub {
305                         if (/$config{wiki_file_prune_regexp}/) {
306                                 $File::Find::prune=1;
307                         }
308                         elsif (! -d $_ && ! -l $_) {
309                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
310                                 if (! defined $f) {
311                                         warn("skipping bad filename $_\n");
312                                 }
313                                 else {
314                                         # Don't add files that are in the
315                                         # srcdir.
316                                         $f=~s/^\Q$config{underlaydir}\E\/?//;
317                                         if (! -e "$config{srcdir}/$f" && 
318                                             ! -l "$config{srcdir}/$f") {
319                                                 push @files, $f;
320                                                 $exists{pagename($f)}=1;
321                                         }
322                                 }
323                         }
324                 },
325         }, $config{underlaydir});
326
327         my %rendered;
328
329         # check for added or removed pages
330         my @add;
331         foreach my $file (@files) {
332                 my $page=pagename($file);
333                 if (! $oldpagemtime{$page}) {
334                         debug("new page $page") unless exists $pagectime{$page};
335                         push @add, $file;
336                         $links{$page}=[];
337                         $pagesources{$page}=$file;
338                         $pagectime{$page}=mtime(srcfile($file))
339                                 unless exists $pagectime{$page};
340                 }
341         }
342         my @del;
343         foreach my $page (keys %oldpagemtime) {
344                 if (! $exists{$page}) {
345                         debug("removing old page $page");
346                         push @del, $pagesources{$page};
347                         prune($config{destdir}."/".$renderedfiles{$page});
348                         delete $renderedfiles{$page};
349                         $oldpagemtime{$page}=0;
350                         delete $pagesources{$page};
351                 }
352         }
353         
354         # render any updated files
355         foreach my $file (@files) {
356                 my $page=pagename($file);
357                 
358                 if (! exists $oldpagemtime{$page} ||
359                     mtime(srcfile($file)) > $oldpagemtime{$page}) {
360                         debug("rendering changed file $file");
361                         render($file);
362                         $rendered{$file}=1;
363                 }
364         }
365         
366         # if any files were added or removed, check to see if each page
367         # needs an update due to linking to them or inlining them.
368         # TODO: inefficient; pages may get rendered above and again here;
369         # problem is the bestlink may have changed and we won't know until
370         # now
371         if (@add || @del) {
372 FILE:           foreach my $file (@files) {
373                         my $page=pagename($file);
374                         foreach my $f (@add, @del) {
375                                 my $p=pagename($f);
376                                 foreach my $link (@{$links{$page}}) {
377                                         if (bestlink($page, $link) eq $p) {
378                                                 debug("rendering $file, which links to $p");
379                                                 render($file);
380                                                 $rendered{$file}=1;
381                                                 next FILE;
382                                         }
383                                 }
384                         }
385                 }
386         }
387
388         # Handle backlinks; if a page has added/removed links, update the
389         # pages it links to. Also handles rebuilding dependat pages.
390         # TODO: inefficient; pages may get rendered above and again here;
391         # problem is the backlinks could be wrong in the first pass render
392         # above
393         if (%rendered || @del) {
394                 foreach my $f (@files) {
395                         my $p=pagename($f);
396                         if (exists $depends{$p}) {
397                                 foreach my $file (keys %rendered, @del) {
398                                         next if $f eq $file;
399                                         my $page=pagename($file);
400                                         if (globlist_match($page, $depends{$p})) {
401                                                 debug("rendering $f, which depends on $page");
402                                                 render($f);
403                                                 $rendered{$f}=1;
404                                                 last;
405                                         }
406                                 }
407                         }
408                 }
409                 
410                 my %linkchanged;
411                 foreach my $file (keys %rendered, @del) {
412                         my $page=pagename($file);
413                         
414                         if (exists $links{$page}) {
415                                 foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) {
416                                         if (length $link &&
417                                             (! exists $oldlinks{$page} ||
418                                              ! grep { bestlink($page, $_) eq $link } @{$oldlinks{$page}})) {
419                                                 $linkchanged{$link}=1;
420                                         }
421                                 }
422                         }
423                         if (exists $oldlinks{$page}) {
424                                 foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) {
425                                         if (length $link &&
426                                             (! exists $links{$page} || 
427                                              ! grep { bestlink($page, $_) eq $link } @{$links{$page}})) {
428                                                 $linkchanged{$link}=1;
429                                         }
430                                 }
431                         }
432                 }
433                 foreach my $link (keys %linkchanged) {
434                         my $linkfile=$pagesources{$link};
435                         if (defined $linkfile) {
436                                 debug("rendering $linkfile, to update its backlinks");
437                                 render($linkfile);
438                                 $rendered{$linkfile}=1;
439                         }
440                 }
441         }
442
443         if (@del && exists $hooks{delete}) {
444                 foreach my $id (keys %{$hooks{delete}}) {
445                         $hooks{delete}{$id}{call}->(@del);
446                 }
447         }
448         if (%rendered && exists $hooks{change}) {
449                 foreach my $id (keys %{$hooks{change}}) {
450                         $hooks{change}{$id}{call}->(keys %rendered);
451                 }
452         }
453 } #}}}
454
455 1