add news item for ikiwiki 1.29
[ikiwiki.git] / IkiWiki / Render.pm
1 #!/usr/bin/perl
2
3 package IkiWiki;
4
5 use warnings;
6 use strict;
7 use IkiWiki;
8 use Encode;
9
10 sub backlinks ($) { #{{{
11         my $page=shift;
12
13         my @links;
14         foreach my $p (keys %links) {
15                 next if bestlink($page, $p) eq $page;
16                 if (grep { length $_ && bestlink($p, $_) eq $page } @{$links{$p}}) {
17                         my $href=abs2rel(htmlpage($p), dirname($page));
18                         
19                         # Trim common dir prefixes from both pages.
20                         my $p_trimmed=$p;
21                         my $page_trimmed=$page;
22                         my $dir;
23                         1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) &&
24                                 defined $dir &&
25                                 $p_trimmed=~s/^\Q$dir\E// &&
26                                 $page_trimmed=~s/^\Q$dir\E//;
27                                        
28                         push @links, { url => $href, page => pagetitle($p_trimmed) };
29                 }
30         }
31
32         return sort { $a->{page} cmp $b->{page} } @links;
33 } #}}}
34
35 sub parentlinks ($) { #{{{
36         my $page=shift;
37         
38         my @ret;
39         my $pagelink="";
40         my $path="";
41         my $skip=1;
42         return if $page eq 'index'; # toplevel
43         foreach my $dir (reverse split("/", $page)) {
44                 if (! $skip) {
45                         $path.="../";
46                         unshift @ret, { url => $path.htmlpage($dir), page => pagetitle($dir) };
47                 }
48                 else {
49                         $skip=0;
50                 }
51         }
52         unshift @ret, { url => length $path ? $path : ".", page => $config{wikiname} };
53         return @ret;
54 } #}}}
55
56 sub genpage ($$$) { #{{{
57         my $page=shift;
58         my $content=shift;
59         my $mtime=shift;
60
61         my $template=template("page.tmpl", blind_cache => 1);
62         my $actions=0;
63
64         if (length $config{cgiurl}) {
65                 $template->param(editurl => cgiurl(do => "edit", page => $page));
66                 $template->param(prefsurl => cgiurl(do => "prefs"));
67                 if ($config{rcs}) {
68                         $template->param(recentchangesurl => cgiurl(do => "recentchanges"));
69                 }
70                 $actions++;
71         }
72
73         if (length $config{historyurl}) {
74                 my $u=$config{historyurl};
75                 $u=~s/\[\[file\]\]/$pagesources{$page}/g;
76                 $template->param(historyurl => $u);
77                 $actions++;
78         }
79         if ($config{discussion}) {
80                 $template->param(discussionlink => htmllink($page, $page, "Discussion", 1, 1));
81                 $actions++;
82         }
83
84         if ($actions) {
85                 $template->param(have_actions => 1);
86         }
87
88         $template->param(
89                 title => $page eq 'index' 
90                         ? $config{wikiname} 
91                         : pagetitle(basename($page)),
92                 wikiname => $config{wikiname},
93                 parentlinks => [parentlinks($page)],
94                 content => $content,
95                 backlinks => [backlinks($page)],
96                 mtime => displaytime($mtime),
97                 baseurl => baseurl($page),
98         );
99
100         run_hooks(pagetemplate => sub {
101                 shift->(page => $page, destpage => $page, template => $template);
102         });
103         
104         $content=$template->output;
105
106         run_hooks(format => sub {
107                 $content=shift->(
108                         page => $page,
109                         content => $content,
110                 );
111         });
112
113         return $content;
114 } #}}}
115
116 sub mtime ($) { #{{{
117         my $file=shift;
118         
119         return (stat($file))[9];
120 } #}}}
121
122 sub findlinks ($$) { #{{{
123         my $page=shift;
124         my $content=shift;
125
126         my @links;
127         while ($content =~ /(?<!\\)$config{wiki_link_regexp}/g) {
128                 push @links, titlepage($2);
129         }
130         if ($config{discussion}) {
131                 # Discussion links are a special case since they're not in the
132                 # text of the page, but on its template.
133                 return @links, "$page/discussion";
134         }
135         else {
136                 return @links;
137         }
138 } #}}}
139
140 sub render ($) { #{{{
141         my $file=shift;
142         
143         my $type=pagetype($file);
144         my $srcfile=srcfile($file);
145         if (defined $type) {
146                 my $content=readfile($srcfile);
147                 my $page=pagename($file);
148                 delete $depends{$page};
149                 will_render($page, htmlpage($page), 1);
150                 
151                 $content=filter($page, $content);
152                 
153                 $links{$page}=[findlinks($page, $content)];
154                 
155                 $content=preprocess($page, $page, $content);
156                 $content=linkify($page, $page, $content);
157                 $content=htmlize($page, $type, $content);
158                 
159                 writefile(htmlpage($page), $config{destdir},
160                         genpage($page, $content, mtime($srcfile)));
161                 $oldpagemtime{$page}=time;
162         }
163         else {
164                 my $content=readfile($srcfile, 1);
165                 $links{$file}=[];
166                 delete $depends{$file};
167                 will_render($file, $file, 1);
168                 writefile($file, $config{destdir}, $content, 1);
169                 $oldpagemtime{$file}=time;
170         }
171 } #}}}
172
173 sub prune ($) { #{{{
174         my $file=shift;
175
176         unlink($file);
177         my $dir=dirname($file);
178         while (rmdir($dir)) {
179                 $dir=dirname($dir);
180         }
181 } #}}}
182
183 sub refresh () { #{{{
184         # find existing pages
185         my %exists;
186         my @files;
187         eval q{use File::Find};
188         find({
189                 no_chdir => 1,
190                 wanted => sub {
191                         $_=decode_utf8($_);
192                         if (/$config{wiki_file_prune_regexp}/) {
193                                 $File::Find::prune=1;
194                         }
195                         elsif (! -d $_ && ! -l $_) {
196                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
197                                 if (! defined $f) {
198                                         warn("skipping bad filename $_\n");
199                                 }
200                                 else {
201                                         $f=~s/^\Q$config{srcdir}\E\/?//;
202                                         push @files, $f;
203                                         $exists{pagename($f)}=1;
204                                 }
205                         }
206                 },
207         }, $config{srcdir});
208         find({
209                 no_chdir => 1,
210                 wanted => sub {
211                         $_=decode_utf8($_);
212                         if (/$config{wiki_file_prune_regexp}/) {
213                                 $File::Find::prune=1;
214                         }
215                         elsif (! -d $_ && ! -l $_) {
216                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
217                                 if (! defined $f) {
218                                         warn("skipping bad filename $_\n");
219                                 }
220                                 else {
221                                         # Don't add files that are in the
222                                         # srcdir.
223                                         $f=~s/^\Q$config{underlaydir}\E\/?//;
224                                         if (! -e "$config{srcdir}/$f" && 
225                                             ! -l "$config{srcdir}/$f") {
226                                                 push @files, $f;
227                                                 $exists{pagename($f)}=1;
228                                         }
229                                 }
230                         }
231                 },
232         }, $config{underlaydir});
233
234         my %rendered;
235
236         # check for added or removed pages
237         my @add;
238         foreach my $file (@files) {
239                 my $page=pagename($file);
240                 if (! $oldpagemtime{$page}) {
241                         debug("new page $page") unless exists $pagectime{$page};
242                         push @add, $file;
243                         $links{$page}=[];
244                         $pagecase{lc $page}=$page;
245                         $pagesources{$page}=$file;
246                         if ($config{getctime} && -e "$config{srcdir}/$file") {
247                                 $pagectime{$page}=rcs_getctime("$config{srcdir}/$file");
248                         }
249                         elsif (! exists $pagectime{$page}) {
250                                 $pagectime{$page}=mtime(srcfile($file));
251                         }
252                 }
253         }
254         my @del;
255         foreach my $page (keys %oldpagemtime) {
256                 if (! $exists{$page}) {
257                         debug("removing old page $page");
258                         push @del, $pagesources{$page};
259                         $renderedfiles{$page}=[];
260                         $oldpagemtime{$page}=0;
261                         prune($config{destdir}."/".$_)
262                                 foreach @{$oldrenderedfiles{$page}};
263                         delete $pagesources{$page};
264                 }
265         }
266         
267         # render any updated files
268         foreach my $file (@files) {
269                 my $page=pagename($file);
270                 
271                 if (! exists $oldpagemtime{$page} ||
272                     mtime(srcfile($file)) > $oldpagemtime{$page} ||
273                     $forcerebuild{$page}) {
274                         debug("rendering $file");
275                         render($file);
276                         $rendered{$file}=1;
277                 }
278         }
279         
280         # if any files were added or removed, check to see if each page
281         # needs an update due to linking to them or inlining them.
282         # TODO: inefficient; pages may get rendered above and again here;
283         # problem is the bestlink may have changed and we won't know until
284         # now
285         if (@add || @del) {
286 FILE:           foreach my $file (@files) {
287                         my $page=pagename($file);
288                         foreach my $f (@add, @del) {
289                                 my $p=pagename($f);
290                                 foreach my $link (@{$links{$page}}) {
291                                         if (bestlink($page, $link) eq $p) {
292                                                 debug("rendering $file, which links to $p");
293                                                 render($file);
294                                                 $rendered{$file}=1;
295                                                 next FILE;
296                                         }
297                                 }
298                         }
299                 }
300         }
301
302         # Handle backlinks; if a page has added/removed links, update the
303         # pages it links to. Also handles rebuilding dependant pages.
304         # TODO: inefficient; pages may get rendered above and again here;
305         # problem is the backlinks could be wrong in the first pass render
306         # above
307         if (%rendered || @del) {
308                 foreach my $f (@files) {
309                         my $p=pagename($f);
310                         if (exists $depends{$p}) {
311                                 foreach my $file (keys %rendered, @del) {
312                                         next if $f eq $file;
313                                         my $page=pagename($file);
314                                         if (pagespec_match($page, $depends{$p})) {
315                                                 debug("rendering $f, which depends on $page");
316                                                 render($f);
317                                                 $rendered{$f}=1;
318                                                 last;
319                                         }
320                                 }
321                         }
322                 }
323                 
324                 my %linkchanged;
325                 foreach my $file (keys %rendered, @del) {
326                         my $page=pagename($file);
327                         
328                         if (exists $links{$page}) {
329                                 foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) {
330                                         if (length $link &&
331                                             (! exists $oldlinks{$page} ||
332                                              ! grep { bestlink($page, $_) eq $link } @{$oldlinks{$page}})) {
333                                                 $linkchanged{$link}=1;
334                                         }
335                                 }
336                         }
337                         if (exists $oldlinks{$page}) {
338                                 foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) {
339                                         if (length $link &&
340                                             (! exists $links{$page} || 
341                                              ! grep { bestlink($page, $_) eq $link } @{$links{$page}})) {
342                                                 $linkchanged{$link}=1;
343                                         }
344                                 }
345                         }
346                 }
347                 foreach my $link (keys %linkchanged) {
348                         my $linkfile=$pagesources{$link};
349                         if (defined $linkfile) {
350                                 debug("rendering $linkfile, to update its backlinks");
351                                 render($linkfile);
352                                 $rendered{$linkfile}=1;
353                         }
354                 }
355         }
356
357         # Remove no longer rendered files.
358         foreach my $src (keys %rendered) {
359                 my $page=pagename($src);
360                 foreach my $file (@{$oldrenderedfiles{$page}}) {
361                         if (! grep { $_ eq $file } @{$renderedfiles{$page}}) {
362                                 debug("removing $file, no longer rendered by $page");
363                                 prune($config{destdir}."/".$file);
364                         }
365                 }
366         }
367
368         if (@del) {
369                 run_hooks(delete => sub { shift->(@del) });
370         }
371         if (%rendered) {
372                 run_hooks(change => sub { shift->(keys %rendered) });
373         }
374 } #}}}
375
376 sub commandline_render () { #{{{
377         loadplugins();
378         checkconfig();
379         lockwiki();
380         loadindex();
381         unlockwiki();
382
383         my $srcfile=possibly_foolish_untaint($config{render});
384         my $file=$srcfile;
385         $file=~s/\Q$config{srcdir}\E\/?//;
386
387         my $type=pagetype($file);
388         die "ikiwiki: cannot render $srcfile\n" unless defined $type;
389         my $content=readfile($srcfile);
390         my $page=pagename($file);
391         $pagesources{$page}=$file;
392         $content=filter($page, $content);
393         $content=preprocess($page, $page, $content);
394         $content=linkify($page, $page, $content);
395         $content=htmlize($page, $type, $content);
396
397         print genpage($page, $content, mtime($srcfile));
398         exit 0;
399 } #}}}
400
401 1