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