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