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