add
[ikiwiki] / ikiwiki
1 #!/usr/bin/perl -T
2
3 use warnings;
4 use strict;
5 use File::Find;
6 use Memoize;
7 use File::Spec;
8
9 BEGIN {
10         $blosxom::version="is a proper perl module too much to ask?";
11         do "/usr/bin/markdown";
12 }
13
14 memoize('pagename');
15 memoize('bestlink');
16
17 my ($srcdir)= shift =~ /(.*)/; # untaint
18 my ($destdir)= shift =~ /(.*)/; # untaint
19 my $link=qr/\[\[([^\s]+)\]\]/;
20 my $verbose=1;
21
22 my %links;
23 my %oldpagemtime;
24 my %renderedfiles;
25
26 sub error ($) {
27         die @_;
28 }
29
30 sub debug ($) {
31         print "@_\n" if $verbose;
32 }
33
34 sub mtime ($) {
35         my $page=shift;
36         
37         return (stat($page))[9];
38 }
39
40 sub basename {
41         my $file=shift;
42
43         $file=~s!.*/!!;
44         return $file;
45 }
46
47 sub dirname {
48         my $file=shift;
49
50         $file=~s!/?[^/]+$!!;
51         return $file;
52 }
53
54 sub pagetype ($) {
55         my $page=shift;
56         
57         if ($page =~ /\.mdwn$/) {
58                 return ".mdwn";
59         }
60         else {
61                 return "unknown";
62         }
63 }
64
65 sub pagename ($) {
66         my $file=shift;
67
68         my $type=pagetype($file);
69         my $page=$file;
70         $page=~s/\Q$type\E*$// unless $type eq 'unknown';
71         return $page;
72 }
73
74 sub htmlpage ($) {
75         my $page=shift;
76
77         return $page.".html";
78 }
79
80 sub readpage ($) {
81         my $page=shift;
82
83         local $/=undef;
84         open (PAGE, "$srcdir/$page") || error("failed to read $page: $!");
85         my $ret=<PAGE>;
86         close PAGE;
87         return $ret;
88 }
89
90 sub writepage ($$) {
91         my $page=shift;
92         my $content=shift;
93
94         my $dir=dirname("$destdir/$page");
95         if (! -d $dir) {
96                 my $d="";
97                 foreach my $s (split(m!/+!, $dir)) {
98                         $d.="$s/";
99                         if (! -d $d) {
100                                 mkdir($d) || error("failed to create directory $d: $!");
101                         }
102                 }
103         }
104         
105         open (PAGE, ">$destdir/$page") || error("failed to write $page: $!");
106         print PAGE $content;
107         close PAGE;
108 }
109
110 sub findlinks {
111         my $content=shift;
112
113         my @links;
114         while ($content =~ /$link/g) {
115                 push @links, lc($1);
116         }
117         return @links;
118 }
119
120 # Given a page and the text of a link on the page, determine which existing
121 # page that link best points to. Prefers pages under a subdirectory with
122 # the same name as the source page, failing that goes down the directory tree
123 # to the base looking for matching pages.
124 sub bestlink ($$) {
125         my $page=shift;
126         my $link=lc(shift);
127         
128         my $cwd=$page;
129         do {
130                 my $l=$cwd;
131                 $l.="/" if length $l;
132                 $l.=$link;
133
134                 if (exists $links{$l}) {
135                         #debug("for $page, \"$link\", use $l");
136                         return $l;
137                 }
138         } while $cwd=~s!/?[^/]+$!!;
139
140         print STDERR "warning: page $page, broken link: $link\n";
141         return "";
142 }
143
144 sub isinlinableimage ($) {
145         my $file=shift;
146         
147         $file=~/\.(png|gif|jpg|jpeg)$/;
148 }
149
150 sub htmllink ($$) {
151         my $page=shift;
152         my $link=shift;
153
154         my $bestlink=bestlink($page, $link);
155
156         return $page if $page eq $bestlink;
157         
158         if (! grep { $_ eq $bestlink } values %renderedfiles) {
159                 $bestlink=htmlpage($bestlink);
160         }
161         if (! grep { $_ eq $bestlink } values %renderedfiles) {
162                 return "<a href=\"?\">?</a>$link"
163         }
164         
165         $bestlink=File::Spec->abs2rel($bestlink, dirname($page));
166         
167         if (isinlinableimage($bestlink)) {
168                 return "<img src=\"$bestlink\">";
169         }
170         return "<a href=\"$bestlink\">$link</a>";
171 }
172
173 sub linkify ($$) {
174         my $content=shift;
175         my $file=shift;
176
177         $content =~ s/$link/htmllink(pagename($file), $1)/eg;
178         
179         return $content;
180 }
181
182 sub htmlize ($$) {
183         my $type=shift;
184         my $content=shift;
185         
186         if ($type eq '.mdwn') {
187                 return Markdown::Markdown($content);
188         }
189         else {
190                 error("htmlization of $type not supported");
191         }
192 }
193
194 sub finalize ($$) {
195         my $content=shift;
196         my $page=shift;
197
198         my $title=basename($page);
199         $title=~s/_/ /g;
200         
201         $content="<html>\n<head><title>$title</title></head>\n<body>\n".
202                   $content.
203                   "</body>\n</html>\n";
204         
205         return $content;
206 }
207
208 sub render ($) {
209         my $file=shift;
210         
211         my $type=pagetype($file);
212         my $content=readpage($file);
213         if ($type ne 'unknown') {
214                 my $page=pagename($file);
215                 $links{$page}=[findlinks($content)];
216         
217                 $content=linkify($content, $file);
218                 $content=htmlize($type, $content);
219                 $content=finalize($content, $page);
220                 
221                 writepage(htmlpage($page), $content);
222                 $oldpagemtime{$page}=time;
223                 $renderedfiles{$page}=htmlpage($page);
224         }
225         else {
226                 $links{$file}=[];
227                 writepage($file, $content);
228                 $oldpagemtime{$file}=time;
229                 $renderedfiles{$file}=$file;
230         }
231 }
232
233 sub loadindex () {
234         open (IN, "$srcdir/.index") || return;
235         while (<IN>) {
236                 chomp;
237                 my ($mtime, $page, $rendered, @links)=split(' ', $_);
238                 $oldpagemtime{$page}=$mtime;
239                 $links{$page}=\@links;
240                 ($renderedfiles{$page})=$rendered=~m/(.*)/; # untaint
241         }
242         close IN;
243 }       
244
245 sub saveindex () {
246         open (OUT, ">$srcdir/.index") || error("cannot write to .index: $!");
247         foreach my $page (keys %oldpagemtime) {
248         print OUT "$oldpagemtime{$page} $page $renderedfiles{$page} ".
249                   join(" ", @{$links{$page}})."\n"
250                         if $oldpagemtime{$page};
251         }
252         close OUT;
253 }
254
255 sub prune ($) {
256         my $file=shift;
257
258         unlink($file);
259         my $dir=dirname($file);
260         while (rmdir($dir)) {
261                 $dir=dirname($dir);
262         }
263 }
264
265 sub refresh () {
266         # Find existing pages.
267         my %exists;
268         my @files;
269         find({
270                 no_chdir => 1,
271                 wanted => sub {
272                         if (/\/\.svn\//) {
273                                 $File::Find::prune=1;
274                         }
275                         elsif (! -d $_ && ! /\.html$/ && ! /\/\./) {
276                                 my ($f)=/(^[-A-Za-z0-9_.:\/+]+$)/; # untaint
277                                 if (! defined $f) {
278                                         warn("skipping bad filename $_\n");
279                                 }
280                                 else {
281                                         $f=~s/^\Q$srcdir\E\/?//;
282                                         push @files, $f;
283                                         $exists{pagename($f)}=1;
284                                 }
285                         }
286                 },
287         }, $srcdir);
288
289         # check for added or removed pages
290         my @adddel;
291         foreach my $file (@files) {
292                 my $page=pagename($file);
293                 if (! $oldpagemtime{$page}) {
294                         debug("new page $page");
295                         push @adddel, $page;
296                         $links{$page}=[];
297                 }
298         }
299         foreach my $page (keys %oldpagemtime) {
300                 if (! $exists{$page}) {
301                         debug("removing old page $page");
302                         prune($destdir."/".$renderedfiles{$page});
303                         delete $renderedfiles{$page};
304                         $oldpagemtime{$page}=0;
305                         push @adddel, $page;
306                 }
307         }
308         
309         # render any updated files
310         foreach my $file (@files) {
311                 my $page=pagename($file);
312                 
313                 if (! exists $oldpagemtime{$page} ||
314                     mtime("$srcdir/$file") > $oldpagemtime{$page}) {
315                         debug("rendering changed file $file");
316                         render($file);
317                 }
318         }
319         
320         # if any files were added or removed, check to see if each page
321         # needs an update due to linking to them
322         if (@adddel) {
323 FILE:           foreach my $file (@files) {
324                         my $page=pagename($file);
325                         foreach my $p (@adddel) {
326                                 foreach my $link (@{$links{$page}}) {
327                                         if (bestlink($page, $link) eq $p) {
328                                                 debug("rendering $file, which links to $p");
329                                                 render($file);
330                                                 next FILE;
331                                         }
332                                 }
333                         }
334                 }
335         }
336 }
337
338 loadindex();
339 refresh();
340 saveindex();