commit changes for email subscriptions
[ikiwiki] / 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 sub htmlize ($$) { #{{{
22         my $type=shift;
23         my $content=shift;
24         
25         if (! $INC{"/usr/bin/markdown"}) {
26                 no warnings 'once';
27                 $blosxom::version="is a proper perl module too much to ask?";
28                 use warnings 'all';
29                 do "/usr/bin/markdown";
30         }
31         
32         if ($type eq '.mdwn') {
33                 return Markdown::Markdown($content);
34         }
35         else {
36                 error("htmlization of $type not supported");
37         }
38 } #}}}
39
40 sub backlinks ($) { #{{{
41         my $page=shift;
42
43         my @links;
44         foreach my $p (keys %links) {
45                 next if bestlink($page, $p) eq $page;
46                 if (grep { length $_ && bestlink($p, $_) eq $page } @{$links{$p}}) {
47                         my $href=File::Spec->abs2rel(htmlpage($p), dirname($page));
48                         
49                         # Trim common dir prefixes from both pages.
50                         my $p_trimmed=$p;
51                         my $page_trimmed=$page;
52                         my $dir;
53                         1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) &&
54                                 defined $dir &&
55                                 $p_trimmed=~s/^\Q$dir\E// &&
56                                 $page_trimmed=~s/^\Q$dir\E//;
57                                        
58                         push @links, { url => $href, page => $p_trimmed };
59                 }
60         }
61
62         return sort { $a->{page} cmp $b->{page} } @links;
63 } #}}}
64
65 sub parentlinks ($) { #{{{
66         my $page=shift;
67         
68         my @ret;
69         my $pagelink="";
70         my $path="";
71         my $skip=1;
72         foreach my $dir (reverse split("/", $page)) {
73                 if (! $skip) {
74                         $path.="../";
75                         unshift @ret, { url => "$path$dir.html", page => $dir };
76                 }
77                 else {
78                         $skip=0;
79                 }
80         }
81         unshift @ret, { url => length $path ? $path : ".", page => $config{wikiname} };
82         return @ret;
83 } #}}}
84
85 sub rsspage ($) { #{{{
86         my $page=shift;
87
88         return $page.".rss";
89 } #}}}
90
91 sub preprocess ($$) { #{{{
92         my $page=shift;
93         my $content=shift;
94
95         my %commands=(inline => \&preprocess_inline);
96         
97         my $handle=sub {
98                 my $escape=shift;
99                 my $command=shift;
100                 my $params=shift;
101                 if (length $escape) {
102                         "[[$command $params]]";
103                 }
104                 elsif (exists $commands{$command}) {
105                         my %params;
106                         while ($params =~ /(\w+)=\"([^"]+)"(\s+|$)/g) {
107                                 $params{$1}=$2;
108                         }
109                         $commands{$command}->($page, %params);
110                 }
111                 else {
112                         "[[bad directive $command]]";
113                 }
114         };
115         
116         $content =~ s{(\\?)$config{wiki_processor_regexp}}{$handle->($1, $2, $3)}eg;
117         return $content;
118 } #}}}
119
120 sub blog_list ($$) { #{{{
121         my $globlist=shift;
122         my $maxitems=shift;
123         
124         my @list;
125         foreach my $page (keys %pagesources) {
126                 if (globlist_match($page, $globlist)) {
127                         push @list, $page;
128                 }
129         }
130
131         @list=sort { $pagectime{$b} <=> $pagectime{$a} } @list;
132         return @list if ! $maxitems || @list <= $maxitems;
133         return @list[0..$maxitems - 1];
134 } #}}}
135
136 sub get_inline_content ($$) { #{{{
137         my $parentpage=shift;
138         my $page=shift;
139         
140         my $file=$pagesources{$page};
141         my $type=pagetype($file);
142         if ($type ne 'unknown') {
143                 return htmlize($type, linkify(readfile(srcfile($file)), $parentpage));
144         }
145         else {
146                 return "";
147         }
148 } #}}}
149
150 sub preprocess_inline ($@) { #{{{
151         my $parentpage=shift;
152         my %params=@_;
153         
154         if (! exists $params{pages}) {
155                 return "";
156         }
157         if (! exists $params{archive}) {
158                 $params{archive}="no";
159         }
160         if (! exists $params{show} && $params{archive} eq "no") {
161                 $params{show}=10;
162         }
163         $inlinepages{$parentpage}=$params{pages};
164
165         my $ret="";
166         
167         if (exists $params{rootpage}) {
168                 my $formtemplate=HTML::Template->new(blind_cache => 1,
169                         filename => "$config{templatedir}/blogpost.tmpl");
170                 $formtemplate->param(cgiurl => $config{cgiurl});
171                 $formtemplate->param(rootpage => $params{rootpage});
172                 my $form=$formtemplate->output;
173                 $ret.=$form;
174         }
175         
176         my $template=HTML::Template->new(blind_cache => 1,
177                 filename => (($params{archive} eq "no") 
178                                 ? "$config{templatedir}/inlinepage.tmpl"
179                                 : "$config{templatedir}/inlinepagetitle.tmpl"));
180         
181         my @pages;
182         foreach my $page (blog_list($params{pages}, $params{show})) {
183                 next if $page eq $parentpage;
184                 push @pages, $page;
185                 $template->param(pagelink => htmllink($parentpage, $page));
186                 $template->param(content => get_inline_content($parentpage, $page))
187                         if $params{archive} eq "no";
188                 $template->param(ctime => scalar(gmtime($pagectime{$page})));
189                 $ret.=$template->output;
190         }
191         
192         # TODO: should really add this to renderedfiles and call
193         # check_overwrite, but currently renderedfiles
194         # only supports listing one file per page.
195         if ($config{rss}) {
196                 writefile(rsspage($parentpage), $config{destdir},
197                         genrss($parentpage, @pages));
198         }
199         
200         return $ret;
201 } #}}}
202
203 sub genpage ($$$) { #{{{
204         my $content=shift;
205         my $page=shift;
206         my $mtime=shift;
207
208         my $title=pagetitle(basename($page));
209         
210         my $template=HTML::Template->new(blind_cache => 1,
211                 filename => "$config{templatedir}/page.tmpl");
212         
213         if (length $config{cgiurl}) {
214                 $template->param(editurl => cgiurl(do => "edit", page => $page));
215                 $template->param(prefsurl => cgiurl(do => "prefs"));
216                 if ($config{rcs}) {
217                         $template->param(recentchangesurl => cgiurl(do => "recentchanges"));
218                 }
219         }
220
221         if (length $config{historyurl}) {
222                 my $u=$config{historyurl};
223                 $u=~s/\[\[file\]\]/$pagesources{$page}/g;
224                 $template->param(historyurl => $u);
225         }
226         if ($config{hyperestraier}) {
227                 $template->param(hyperestraierurl => cgiurl());
228         }
229
230         if ($config{rss} && $inlinepages{$page}) {
231                 $template->param(rssurl => rsspage(basename($page)));
232         }
233         
234         $template->param(
235                 title => $title,
236                 wikiname => $config{wikiname},
237                 parentlinks => [parentlinks($page)],
238                 content => $content,
239                 backlinks => [backlinks($page)],
240                 discussionlink => htmllink($page, "Discussion", 1, 1),
241                 mtime => scalar(gmtime($mtime)),
242                 styleurl => styleurl($page),
243         );
244         
245         return $template->output;
246 } #}}}
247
248 sub date_822 ($) { #{{{
249         my $time=shift;
250
251         eval q{use POSIX};
252         return POSIX::strftime("%a, %d %b %Y %H:%M:%S %z", localtime($time));
253 } #}}}
254
255 sub absolute_urls ($$) { #{{{
256         # sucky sub because rss sucks
257         my $content=shift;
258         my $url=shift;
259
260         $url=~s/[^\/]+$//;
261         
262         $content=~s/<a\s+href="(?!http:\/\/)([^"]+)"/<a href="$url$1"/ig;
263         $content=~s/<img\s+src="(?!http:\/\/)([^"]+)"/<img src="$url$1"/ig;
264         return $content;
265 } #}}}
266
267 sub genrss ($@) { #{{{
268         my $page=shift;
269         my @pages=@_;
270         
271         my $url="$config{url}/".htmlpage($page);
272         
273         my $template=HTML::Template->new(blind_cache => 1,
274                 filename => "$config{templatedir}/rsspage.tmpl");
275         
276         my @items;
277         foreach my $p (@pages) {
278                 push @items, {
279                         itemtitle => pagetitle(basename($p)),
280                         itemurl => "$config{url}/$renderedfiles{$p}",
281                         itempubdate => date_822($pagectime{$p}),
282                         itemcontent => absolute_urls(get_inline_content($page, $p), $url),
283                 } if exists $renderedfiles{$p};
284         }
285
286         $template->param(
287                 title => $config{wikiname},
288                 pageurl => $url,
289                 items => \@items,
290         );
291         
292         return $template->output;
293 } #}}}
294
295 sub check_overwrite ($$) { #{{{
296         # Important security check. Make sure to call this before saving
297         # any files to the source directory.
298         my $dest=shift;
299         my $src=shift;
300         
301         if (! exists $renderedfiles{$src} && -e $dest && ! $config{rebuild}) {
302                 error("$dest already exists and was rendered from ".
303                         join(" ",(grep { $renderedfiles{$_} eq $dest } keys
304                                 %renderedfiles)).
305                         ", before, so not rendering from $src");
306         }
307 } #}}}
308
309 sub mtime ($) { #{{{
310         my $file=shift;
311         
312         return (stat($file))[9];
313 } #}}}
314
315 sub findlinks ($$) { #{{{
316         my $content=shift;
317         my $page=shift;
318
319         my @links;
320         while ($content =~ /(?<!\\)$config{wiki_link_regexp}/g) {
321                 push @links, titlepage($2);
322         }
323         # Discussion links are a special case since they're not in the text
324         # of the page, but on its template.
325         return @links, "$page/discussion";
326 } #}}}
327
328 sub render ($) { #{{{
329         my $file=shift;
330         
331         my $type=pagetype($file);
332         my $srcfile=srcfile($file);
333         if ($type ne 'unknown') {
334                 my $content=readfile($srcfile);
335                 my $page=pagename($file);
336                 
337                 $links{$page}=[findlinks($content, $page)];
338                 delete $inlinepages{$page};
339                 
340                 $content=linkify($content, $page);
341                 $content=preprocess($page, $content);
342                 $content=htmlize($type, $content);
343                 
344                 check_overwrite("$config{destdir}/".htmlpage($page), $page);
345                 writefile(htmlpage($page), $config{destdir},
346                         genpage($content, $page, mtime($srcfile)));
347                 $oldpagemtime{$page}=time;
348                 $renderedfiles{$page}=htmlpage($page);
349         }
350         else {
351                 my $content=readfile($srcfile, 1);
352                 $links{$file}=[];
353                 check_overwrite("$config{destdir}/$file", $file);
354                 writefile($file, $config{destdir}, $content, 1);
355                 $oldpagemtime{$file}=time;
356                 $renderedfiles{$file}=$file;
357         }
358 } #}}}
359
360 sub prune ($) { #{{{
361         my $file=shift;
362
363         unlink($file);
364         my $dir=dirname($file);
365         while (rmdir($dir)) {
366                 $dir=dirname($dir);
367         }
368 } #}}}
369
370 sub estcfg () { #{{{
371         my $estdir="$config{wikistatedir}/hyperestraier";
372         my $cgi=basename($config{cgiurl});
373         $cgi=~s/\..*$//;
374         open(TEMPLATE, ">$estdir/$cgi.tmpl") ||
375                 error("write $estdir/$cgi.tmpl: $!");
376         print TEMPLATE misctemplate("search", 
377                 "<!--ESTFORM-->\n\n<!--ESTRESULT-->\n\n<!--ESTINFO-->\n\n");
378         close TEMPLATE;
379         open(TEMPLATE, ">$estdir/$cgi.conf") ||
380                 error("write $estdir/$cgi.conf: $!");
381         my $template=HTML::Template->new(
382                 filename => "$config{templatedir}/estseek.conf"
383         );
384         eval q{use Cwd 'abs_path'};
385         $template->param(
386                 index => $estdir,
387                 tmplfile => "$estdir/$cgi.tmpl",
388                 destdir => abs_path($config{destdir}),
389                 url => $config{url},
390         );
391         print TEMPLATE $template->output;
392         close TEMPLATE;
393         $cgi="$estdir/".basename($config{cgiurl});
394         unlink($cgi);
395         symlink("/usr/lib/estraier/estseek.cgi", $cgi) ||
396                 error("symlink $cgi: $!");
397 } # }}}
398
399 sub estcmd ($;@) { #{{{
400         my @params=split(' ', shift);
401         push @params, "-cl", "$config{wikistatedir}/hyperestraier";
402         if (@_) {
403                 push @params, "-";
404         }
405         
406         my $pid=open(CHILD, "|-");
407         if ($pid) {
408                 # parent
409                 foreach (@_) {
410                         print CHILD "$_\n";
411                 }
412                 close(CHILD) || error("estcmd @params exited nonzero: $?");
413         }
414         else {
415                 # child
416                 open(STDOUT, "/dev/null"); # shut it up (closing won't work)
417                 exec("estcmd", @params) || error("can't run estcmd");
418         }
419 } #}}}
420
421 sub refresh () { #{{{
422         # find existing pages
423         my %exists;
424         my @files;
425         eval q{use File::Find};
426         find({
427                 no_chdir => 1,
428                 wanted => sub {
429                         if (/$config{wiki_file_prune_regexp}/) {
430                                 $File::Find::prune=1;
431                         }
432                         elsif (! -d $_ && ! -l $_) {
433                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
434                                 if (! defined $f) {
435                                         warn("skipping bad filename $_\n");
436                                 }
437                                 else {
438                                         $f=~s/^\Q$config{srcdir}\E\/?//;
439                                         push @files, $f;
440                                         $exists{pagename($f)}=1;
441                                 }
442                         }
443                 },
444         }, $config{srcdir});
445         find({
446                 no_chdir => 1,
447                 wanted => sub {
448                         if (/$config{wiki_file_prune_regexp}/) {
449                                 $File::Find::prune=1;
450                         }
451                         elsif (! -d $_ && ! -l $_) {
452                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
453                                 if (! defined $f) {
454                                         warn("skipping bad filename $_\n");
455                                 }
456                                 else {
457                                         # Don't add files that are in the
458                                         # srcdir.
459                                         $f=~s/^\Q$config{underlaydir}\E\/?//;
460                                         if (! -e "$config{srcdir}/$f" && 
461                                             ! -l "$config{srcdir}/$f") {
462                                                 push @files, $f;
463                                                 $exists{pagename($f)}=1;
464                                         }
465                                 }
466                         }
467                 },
468         }, $config{underlaydir});
469
470         my %rendered;
471
472         # check for added or removed pages
473         my @add;
474         foreach my $file (@files) {
475                 my $page=pagename($file);
476                 if (! $oldpagemtime{$page}) {
477                         debug("new page $page") unless exists $pagectime{$page};
478                         push @add, $file;
479                         $links{$page}=[];
480                         $pagesources{$page}=$file;
481                         $pagectime{$page}=mtime(srcfile($file))
482                                 unless exists $pagectime{$page};
483                 }
484         }
485         my @del;
486         foreach my $page (keys %oldpagemtime) {
487                 if (! $exists{$page}) {
488                         debug("removing old page $page");
489                         push @del, $pagesources{$page};
490                         prune($config{destdir}."/".$renderedfiles{$page});
491                         delete $renderedfiles{$page};
492                         $oldpagemtime{$page}=0;
493                         delete $pagesources{$page};
494                 }
495         }
496         
497         # render any updated files
498         foreach my $file (@files) {
499                 my $page=pagename($file);
500                 
501                 if (! exists $oldpagemtime{$page} ||
502                     mtime(srcfile($file)) > $oldpagemtime{$page}) {
503                         debug("rendering changed file $file");
504                         render($file);
505                         $rendered{$file}=1;
506                 }
507         }
508         
509         # if any files were added or removed, check to see if each page
510         # needs an update due to linking to them or inlining them.
511         # TODO: inefficient; pages may get rendered above and again here;
512         # problem is the bestlink may have changed and we won't know until
513         # now
514         if (@add || @del) {
515 FILE:           foreach my $file (@files) {
516                         my $page=pagename($file);
517                         foreach my $f (@add, @del) {
518                                 my $p=pagename($f);
519                                 foreach my $link (@{$links{$page}}) {
520                                         if (bestlink($page, $link) eq $p) {
521                                                 debug("rendering $file, which links to $p");
522                                                 render($file);
523                                                 $rendered{$file}=1;
524                                                 next FILE;
525                                         }
526                                 }
527                         }
528                 }
529         }
530
531         # Handle backlinks; if a page has added/removed links, update the
532         # pages it links to. Also handle inlining here.
533         # TODO: inefficient; pages may get rendered above and again here;
534         # problem is the backlinks could be wrong in the first pass render
535         # above
536         if (%rendered || @del) {
537                 foreach my $f (@files) {
538                         my $p=pagename($f);
539                         if (exists $inlinepages{$p}) {
540                                 foreach my $file (keys %rendered, @del) {
541                                         my $page=pagename($file);
542                                         if (globlist_match($page, $inlinepages{$p})) {
543                                                 debug("rendering $f, which inlines $page");
544                                                 render($f);
545                                                 $rendered{$f}=1;
546                                                 last;
547                                         }
548                                 }
549                         }
550                 }
551                 
552                 my %linkchanged;
553                 foreach my $file (keys %rendered, @del) {
554                         my $page=pagename($file);
555                         
556                         if (exists $links{$page}) {
557                                 foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) {
558                                         if (length $link &&
559                                             ! exists $oldlinks{$page} ||
560                                             ! grep { $_ eq $link } @{$oldlinks{$page}}) {
561                                                 $linkchanged{$link}=1;
562                                         }
563                                 }
564                         }
565                         if (exists $oldlinks{$page}) {
566                                 foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) {
567                                         if (length $link &&
568                                             ! exists $links{$page} ||
569                                             ! grep { $_ eq $link } @{$links{$page}}) {
570                                                 $linkchanged{$link}=1;
571                                         }
572                                 }
573                         }
574                 }
575                 foreach my $link (keys %linkchanged) {
576                         my $linkfile=$pagesources{$link};
577                         if (defined $linkfile) {
578                                 debug("rendering $linkfile, to update its backlinks");
579                                 render($linkfile);
580                                 $rendered{$linkfile}=1;
581                         }
582                 }
583         }
584
585         if ($config{hyperestraier} && (%rendered || @del)) {
586                 debug("updating hyperestraier search index");
587                 if (%rendered) {
588                         estcmd("gather -cm -bc -cl -sd", 
589                                 map { $config{destdir}."/".$renderedfiles{pagename($_)} }
590                                 keys %rendered);
591                 }
592                 if (@del) {
593                         estcmd("purge -cl");
594                 }
595                 
596                 debug("generating hyperestraier cgi config");
597                 estcfg();
598         }
599 } #}}}
600
601 1