]> sipb.mit.edu Git - ikiwiki.git/blob - IkiWiki/Render.pm
* Add an orphans plugin for finding pages that nothing links to.
[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
9 sub linkify ($$) { #{{{
10         my $content=shift;
11         my $page=shift;
12
13         $content =~ s{(\\?)$config{wiki_link_regexp}}{
14                 $2 ? ( $1 ? "[[$2|$3]]" : htmllink($page, titlepage($3), 0, 0, pagetitle($2)))
15                    : ( $1 ? "[[$3]]" :    htmllink($page, titlepage($3)))
16         }eg;
17         
18         return $content;
19 } #}}}
20
21 my $_scrubber;
22 sub scrubber { #{{{
23         return $_scrubber if defined $_scrubber;
24         
25         eval q{use HTML::Scrubber};
26         # Lists based on http://feedparser.org/docs/html-sanitization.html
27         $_scrubber = HTML::Scrubber->new(
28                 allow => [qw{
29                         a abbr acronym address area b big blockquote br
30                         button caption center cite code col colgroup dd del
31                         dfn dir div dl dt em fieldset font form h1 h2 h3 h4
32                         h5 h6 hr i img input ins kbd label legend li map
33                         menu ol optgroup option p pre q s samp select small
34                         span strike strong sub sup table tbody td textarea
35                         tfoot th thead tr tt u ul var
36                 }],
37                 default => [undef, { map { $_ => 1 } qw{
38                         abbr accept accept-charset accesskey action
39                         align alt axis border cellpadding cellspacing
40                         char charoff charset checked cite class
41                         clear cols colspan color compact coords
42                         datetime dir disabled enctype for frame
43                         headers height href hreflang hspace id ismap
44                         label lang longdesc maxlength media method
45                         multiple name nohref noshade nowrap prompt
46                         readonly rel rev rows rowspan rules scope
47                         selected shape size span src start summary
48                         tabindex target title type usemap valign
49                         value vspace width
50                 }}],
51         );
52         return $_scrubber;
53 } # }}}
54
55 sub htmlize ($$) { #{{{
56         my $type=shift;
57         my $content=shift;
58         
59         if (! $INC{"/usr/bin/markdown"}) {
60                 no warnings 'once';
61                 $blosxom::version="is a proper perl module too much to ask?";
62                 use warnings 'all';
63                 do "/usr/bin/markdown";
64         }
65         
66         if ($type eq '.mdwn') {
67                 $content=Markdown::Markdown($content);
68         }
69         else {
70                 error("htmlization of $type not supported");
71         }
72
73         if ($config{sanitize}) {
74                 $content=scrubber()->scrub($content);
75         }
76         
77         return $content;
78 } #}}}
79
80 sub backlinks ($) { #{{{
81         my $page=shift;
82
83         my @links;
84         foreach my $p (keys %links) {
85                 next if bestlink($page, $p) eq $page;
86                 if (grep { length $_ && bestlink($p, $_) eq $page } @{$links{$p}}) {
87                         my $href=File::Spec->abs2rel(htmlpage($p), dirname($page));
88                         
89                         # Trim common dir prefixes from both pages.
90                         my $p_trimmed=$p;
91                         my $page_trimmed=$page;
92                         my $dir;
93                         1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) &&
94                                 defined $dir &&
95                                 $p_trimmed=~s/^\Q$dir\E// &&
96                                 $page_trimmed=~s/^\Q$dir\E//;
97                                        
98                         push @links, { url => $href, page => $p_trimmed };
99                 }
100         }
101
102         return sort { $a->{page} cmp $b->{page} } @links;
103 } #}}}
104
105 sub parentlinks ($) { #{{{
106         my $page=shift;
107         
108         my @ret;
109         my $pagelink="";
110         my $path="";
111         my $skip=1;
112         foreach my $dir (reverse split("/", $page)) {
113                 if (! $skip) {
114                         $path.="../";
115                         unshift @ret, { url => "$path$dir.html", page => $dir };
116                 }
117                 else {
118                         $skip=0;
119                 }
120         }
121         unshift @ret, { url => length $path ? $path : ".", page => $config{wikiname} };
122         return @ret;
123 } #}}}
124
125 sub preprocess ($$) { #{{{
126         my $page=shift;
127         my $content=shift;
128
129         my $handle=sub {
130                 my $escape=shift;
131                 my $command=shift;
132                 my $params=shift;
133                 if (length $escape) {
134                         return "[[$command $params]]";
135                 }
136                 elsif (exists $plugins{preprocess}{$command}) {
137                         my %params;
138                         while ($params =~ /(\w+)=\"([^"]+)"(\s+|$)/g) {
139                                 $params{$1}=$2;
140                         }
141                         return $plugins{preprocess}{$command}->(page => $page, %params);
142                 }
143                 else {
144                         return "[[$command not processed]]";
145                 }
146         };
147         
148         $content =~ s{(\\?)$config{wiki_processor_regexp}}{$handle->($1, $2, $3)}eg;
149         return $content;
150 } #}}}
151
152 sub add_depends ($$) { #{{{
153         my $page=shift;
154         my $globlist=shift;
155         
156         if (! exists $depends{$page}) {
157                 $depends{$page}=$globlist;
158         }
159         else {
160                 $depends{$page}.=" ".$globlist;
161         }
162 } # }}}
163
164 sub genpage ($$$) { #{{{
165         my $content=shift;
166         my $page=shift;
167         my $mtime=shift;
168
169         my $title=pagetitle(basename($page));
170         
171         my $template=HTML::Template->new(blind_cache => 1,
172                 filename => "$config{templatedir}/page.tmpl");
173         
174         if (length $config{cgiurl}) {
175                 $template->param(editurl => cgiurl(do => "edit", page => $page));
176                 $template->param(prefsurl => cgiurl(do => "prefs"));
177                 if ($config{rcs}) {
178                         $template->param(recentchangesurl => cgiurl(do => "recentchanges"));
179                 }
180         }
181
182         if (length $config{historyurl}) {
183                 my $u=$config{historyurl};
184                 $u=~s/\[\[file\]\]/$pagesources{$page}/g;
185                 $template->param(historyurl => $u);
186         }
187         if ($config{hyperestraier}) {
188                 $template->param(hyperestraierurl => cgiurl());
189         }
190
191         $template->param(
192                 title => $title,
193                 wikiname => $config{wikiname},
194                 parentlinks => [parentlinks($page)],
195                 content => $content,
196                 backlinks => [backlinks($page)],
197                 discussionlink => htmllink($page, "Discussion", 1, 1),
198                 mtime => scalar(gmtime($mtime)),
199                 styleurl => styleurl($page),
200         );
201         
202         return $template->output;
203 } #}}}
204
205 sub check_overwrite ($$) { #{{{
206         # Important security check. Make sure to call this before saving
207         # any files to the source directory.
208         my $dest=shift;
209         my $src=shift;
210         
211         if (! exists $renderedfiles{$src} && -e $dest && ! $config{rebuild}) {
212                 error("$dest already exists and was rendered from ".
213                         join(" ",(grep { $renderedfiles{$_} eq $dest } keys
214                                 %renderedfiles)).
215                         ", before, so not rendering from $src");
216         }
217 } #}}}
218
219 sub mtime ($) { #{{{
220         my $file=shift;
221         
222         return (stat($file))[9];
223 } #}}}
224
225 sub findlinks ($$) { #{{{
226         my $content=shift;
227         my $page=shift;
228
229         my @links;
230         while ($content =~ /(?<!\\)$config{wiki_link_regexp}/g) {
231                 push @links, titlepage($2);
232         }
233         # Discussion links are a special case since they're not in the text
234         # of the page, but on its template.
235         return @links, "$page/discussion";
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                 
247                 $links{$page}=[findlinks($content, $page)];
248                 delete $depends{$page};
249                 
250                 $content=linkify($content, $page);
251                 $content=preprocess($page, $content);
252                 $content=htmlize($type, $content);
253                 
254                 check_overwrite("$config{destdir}/".htmlpage($page), $page);
255                 writefile(htmlpage($page), $config{destdir},
256                         genpage($content, $page, mtime($srcfile)));
257                 $oldpagemtime{$page}=time;
258                 $renderedfiles{$page}=htmlpage($page);
259         }
260         else {
261                 my $content=readfile($srcfile, 1);
262                 $links{$file}=[];
263                 delete $depends{$file};
264                 check_overwrite("$config{destdir}/$file", $file);
265                 writefile($file, $config{destdir}, $content, 1);
266                 $oldpagemtime{$file}=time;
267                 $renderedfiles{$file}=$file;
268         }
269 } #}}}
270
271 sub prune ($) { #{{{
272         my $file=shift;
273
274         unlink($file);
275         my $dir=dirname($file);
276         while (rmdir($dir)) {
277                 $dir=dirname($dir);
278         }
279 } #}}}
280
281 sub estcfg () { #{{{
282         my $estdir="$config{wikistatedir}/hyperestraier";
283         my $cgi=basename($config{cgiurl});
284         $cgi=~s/\..*$//;
285         open(TEMPLATE, ">$estdir/$cgi.tmpl") ||
286                 error("write $estdir/$cgi.tmpl: $!");
287         print TEMPLATE misctemplate("search", 
288                 "<!--ESTFORM-->\n\n<!--ESTRESULT-->\n\n<!--ESTINFO-->\n\n");
289         close TEMPLATE;
290         open(TEMPLATE, ">$estdir/$cgi.conf") ||
291                 error("write $estdir/$cgi.conf: $!");
292         my $template=HTML::Template->new(
293                 filename => "$config{templatedir}/estseek.conf"
294         );
295         eval q{use Cwd 'abs_path'};
296         $template->param(
297                 index => $estdir,
298                 tmplfile => "$estdir/$cgi.tmpl",
299                 destdir => abs_path($config{destdir}),
300                 url => $config{url},
301         );
302         print TEMPLATE $template->output;
303         close TEMPLATE;
304         $cgi="$estdir/".basename($config{cgiurl});
305         unlink($cgi);
306         symlink("/usr/lib/estraier/estseek.cgi", $cgi) ||
307                 error("symlink $cgi: $!");
308 } # }}}
309
310 sub estcmd ($;@) { #{{{
311         my @params=split(' ', shift);
312         push @params, "-cl", "$config{wikistatedir}/hyperestraier";
313         if (@_) {
314                 push @params, "-";
315         }
316         
317         my $pid=open(CHILD, "|-");
318         if ($pid) {
319                 # parent
320                 foreach (@_) {
321                         print CHILD "$_\n";
322                 }
323                 close(CHILD) || error("estcmd @params exited nonzero: $?");
324         }
325         else {
326                 # child
327                 open(STDOUT, "/dev/null"); # shut it up (closing won't work)
328                 exec("estcmd", @params) || error("can't run estcmd");
329         }
330 } #}}}
331
332 sub refresh () { #{{{
333         # find existing pages
334         my %exists;
335         my @files;
336         eval q{use File::Find};
337         find({
338                 no_chdir => 1,
339                 wanted => sub {
340                         if (/$config{wiki_file_prune_regexp}/) {
341                                 $File::Find::prune=1;
342                         }
343                         elsif (! -d $_ && ! -l $_) {
344                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
345                                 if (! defined $f) {
346                                         warn("skipping bad filename $_\n");
347                                 }
348                                 else {
349                                         $f=~s/^\Q$config{srcdir}\E\/?//;
350                                         push @files, $f;
351                                         $exists{pagename($f)}=1;
352                                 }
353                         }
354                 },
355         }, $config{srcdir});
356         find({
357                 no_chdir => 1,
358                 wanted => sub {
359                         if (/$config{wiki_file_prune_regexp}/) {
360                                 $File::Find::prune=1;
361                         }
362                         elsif (! -d $_ && ! -l $_) {
363                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
364                                 if (! defined $f) {
365                                         warn("skipping bad filename $_\n");
366                                 }
367                                 else {
368                                         # Don't add files that are in the
369                                         # srcdir.
370                                         $f=~s/^\Q$config{underlaydir}\E\/?//;
371                                         if (! -e "$config{srcdir}/$f" && 
372                                             ! -l "$config{srcdir}/$f") {
373                                                 push @files, $f;
374                                                 $exists{pagename($f)}=1;
375                                         }
376                                 }
377                         }
378                 },
379         }, $config{underlaydir});
380
381         my %rendered;
382
383         # check for added or removed pages
384         my @add;
385         foreach my $file (@files) {
386                 my $page=pagename($file);
387                 if (! $oldpagemtime{$page}) {
388                         debug("new page $page") unless exists $pagectime{$page};
389                         push @add, $file;
390                         $links{$page}=[];
391                         $pagesources{$page}=$file;
392                         $pagectime{$page}=mtime(srcfile($file))
393                                 unless exists $pagectime{$page};
394                 }
395         }
396         my @del;
397         foreach my $page (keys %oldpagemtime) {
398                 if (! $exists{$page}) {
399                         debug("removing old page $page");
400                         push @del, $pagesources{$page};
401                         prune($config{destdir}."/".$renderedfiles{$page});
402                         delete $renderedfiles{$page};
403                         $oldpagemtime{$page}=0;
404                         delete $pagesources{$page};
405                 }
406         }
407         
408         # render any updated files
409         foreach my $file (@files) {
410                 my $page=pagename($file);
411                 
412                 if (! exists $oldpagemtime{$page} ||
413                     mtime(srcfile($file)) > $oldpagemtime{$page}) {
414                         debug("rendering changed file $file");
415                         render($file);
416                         $rendered{$file}=1;
417                 }
418         }
419         
420         # if any files were added or removed, check to see if each page
421         # needs an update due to linking to them or inlining them.
422         # TODO: inefficient; pages may get rendered above and again here;
423         # problem is the bestlink may have changed and we won't know until
424         # now
425         if (@add || @del) {
426 FILE:           foreach my $file (@files) {
427                         my $page=pagename($file);
428                         foreach my $f (@add, @del) {
429                                 my $p=pagename($f);
430                                 foreach my $link (@{$links{$page}}) {
431                                         if (bestlink($page, $link) eq $p) {
432                                                 debug("rendering $file, which links to $p");
433                                                 render($file);
434                                                 $rendered{$file}=1;
435                                                 next FILE;
436                                         }
437                                 }
438                         }
439                 }
440         }
441
442         # Handle backlinks; if a page has added/removed links, update the
443         # pages it links to. Also handles rebuilding dependat pages.
444         # TODO: inefficient; pages may get rendered above and again here;
445         # problem is the backlinks could be wrong in the first pass render
446         # above
447         if (%rendered || @del) {
448                 foreach my $f (@files) {
449                         my $p=pagename($f);
450                         if (exists $depends{$p}) {
451                                 foreach my $file (keys %rendered, @del) {
452                                         next if $f eq $file;
453                                         my $page=pagename($file);
454                                         if (globlist_match($page, $depends{$p})) {
455                                                 debug("rendering $f, which depends on $page");
456                                                 render($f);
457                                                 $rendered{$f}=1;
458                                                 last;
459                                         }
460                                 }
461                         }
462                 }
463                 
464                 my %linkchanged;
465                 foreach my $file (keys %rendered, @del) {
466                         my $page=pagename($file);
467                         
468                         if (exists $links{$page}) {
469                                 foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) {
470                                         if (length $link &&
471                                             (! exists $oldlinks{$page} ||
472                                              ! grep { bestlink($page, $_) eq $link } @{$oldlinks{$page}})) {
473                                                 $linkchanged{$link}=1;
474                                         }
475                                 }
476                         }
477                         if (exists $oldlinks{$page}) {
478                                 foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) {
479                                         if (length $link &&
480                                             (! exists $links{$page} || 
481                                              ! grep { bestlink($page, $_) eq $link } @{$links{$page}})) {
482                                                 $linkchanged{$link}=1;
483                                         }
484                                 }
485                         }
486                 }
487                 foreach my $link (keys %linkchanged) {
488                         my $linkfile=$pagesources{$link};
489                         if (defined $linkfile) {
490                                 debug("rendering $linkfile, to update its backlinks");
491                                 render($linkfile);
492                                 $rendered{$linkfile}=1;
493                         }
494                 }
495         }
496
497         if ($config{hyperestraier} && (%rendered || @del)) {
498                 debug("updating hyperestraier search index");
499                 if (%rendered) {
500                         estcmd("gather -cm -bc -cl -sd", 
501                                 map { $config{destdir}."/".$renderedfiles{pagename($_)} }
502                                 keys %rendered);
503                 }
504                 if (@del) {
505                         estcmd("purge -cl");
506                 }
507                 
508                 debug("generating hyperestraier cgi config");
509                 estcfg();
510         }
511 } #}}}
512
513 1