Add table of contents to documentation pages
[exim-website.git] / templates / gen.pl
1 #!/usr/bin/perl
2 use strict;
3 use warnings;
4 use File::Copy;
5 use File::Find;
6 use File::Spec;
7 use XML::LibXML;
8 use XML::LibXSLT;
9
10 my $canonical_url = 'http://www.exim.org/';
11
12 ## Parse arguments
13   my %opt = parse_arguments();
14
15 ## Generate the pages
16   do_doc( 'spec',   $_ ) foreach @{$opt{spec}||[]};
17   do_doc( 'filter', $_ ) foreach @{$opt{filter}||[]};
18   do_web(              ) if exists $opt{web};
19
20 ## Add the exim-html-current symlink
21   print "Symlinking exim-html-current to exim-html-$opt{latest}\n";
22   symlink( "$opt{docroot}/exim-html-$opt{latest}", "$opt{docroot}/exim-html-current" );
23
24 ## Generate the website files
25   sub do_web {
26
27      ## Make sure the template web directory exists
28        die "No such directory: $opt{tmpl}/web\n" unless -d "$opt{tmpl}/web";
29
30      ## Scan the web templates
31        find(sub{
32           my( $path ) = substr( $File::Find::name, length("$opt{tmpl}/web"), length($File::Find::name) ) =~ m#^/*(.*)$#;
33
34           if( -d "$opt{tmpl}/web/$path" ){
35
36              ## Create the directory in the doc root if it doesn't exist
37                if( ! -d "$opt{docroot}/$path" ){
38                   mkdir( "$opt{docroot}/$path" ) or die "Unable to make $opt{docroot}/$path: $!\n";
39                }
40
41           } else {
42
43              ## Build HTML from XSL files and simply copy static files which have changed
44                if( $path =~ /(.+)\.xsl$/ ){
45                   print "Generating  : docroot:/$1.html\n";
46                   transform( undef, "$opt{tmpl}/web/$path", "$opt{docroot}/$1.html" );
47                } elsif( -f "$opt{tmpl}/web/$path" ){
48
49                   ## Skip if the file hasn't changed (mtime based)
50                     return if -f "$opt{docroot}/$path" && (stat("$opt{tmpl}/web/$path"))[9] == (stat("$opt{docroot}/$path"))[9];
51
52                   ## Copy
53                     print "Copying to  : docroot:/$path\n";
54                     copy( "$opt{tmpl}/web/$path", "$opt{docroot}/$path" ) or die "$path: $!";
55
56                   ## Set mtime
57                     utime( time, (stat("$opt{tmpl}/web/$path"))[9], "$opt{docroot}/$path" );
58                }
59           }
60
61        }, "$opt{tmpl}/web");
62   }
63
64 ## Generate index/chapter files for a doc
65   sub do_doc {
66      my( $type, $xml_path ) = @_;
67
68      ## Read and validate the XML file
69        my $xml = XML::LibXML->new()->parse_file( $xml_path ) or die $!;
70
71      ## Get the version number
72        my $version = $xml->findvalue('/book/bookinfo/revhistory/revision/revnumber');
73        die "Unable to get version number\n" unless defined $version && $version =~ /^\d+(\.\d+)*$/;
74
75      ## Prepend chapter filenames?
76        my $prepend_chapter = $type eq 'filter' ? 'filter_' : '';
77
78      ## Add the canonical url for this document
79        $xml->documentElement()->appendTextChild('canonical_url',"${canonical_url}exim-html-current/doc/html/spec_html/".($type eq 'spec'?'index':'filter').".html");
80
81      ## Fixup the XML
82        xref_fixup( $xml, $prepend_chapter );
83
84      ## Generate the front page
85        {
86           my $path = "exim-html-$version/doc/html/spec_html/".($type eq 'filter'?$type:'index').".html";
87           print "Generating  : docroot:/$path\n";
88           transform( $xml,
89              "$opt{tmpl}/doc/index.xsl",
90              "$opt{docroot}/$path",
91           );
92        }
93
94      ## Generate a Table of Contents XML file
95        {
96           my $path = "exim-html-$version/doc/html/spec_html/".($type eq 'filter'?'filter_toc':'index_toc').".xml";
97           print "Generating  : docroot:/$path\n";
98           transform( $xml,
99              "$opt{tmpl}/doc/toc.xsl",
100              "$opt{docroot}/$path",
101           );
102        }
103
104      ## Generate the chapters
105        my $counter = 0;
106        foreach my $chapter ( map {$_->cloneNode(1)} $xml->findnodes('/book/chapter') ){
107
108           ## Add a <chapter_id>N</chapter_id> node for the stylesheet to use
109             $chapter->appendTextChild( 'chapter_id', ++$counter );
110
111           ## Add previous/next/canonical urls for nav
112             {
113                $chapter->appendTextChild( 'prev_url',
114                   $counter == 1
115                     ? $type eq 'filter'
116                       ? 'filter.html'
117                       : 'index.html'
118                     : sprintf('%sch%02d.html',$prepend_chapter,$counter-1)
119                );
120                $chapter->appendTextChild( 'next_url',      sprintf('%sch%02d.html',$prepend_chapter,$counter+1) );
121                $chapter->appendTextChild( 'canonical_url', sprintf('http://www.exim.org/exim-html-current/doc/html/spec_html/%sch%02d.html',$prepend_chapter,$counter) );
122             }
123
124           ## Create an XML document from the chapter
125             my $doc = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
126             $doc->setDocumentElement( $chapter );
127
128           ## Transform the chapter into html
129             {
130                my $path = sprintf('exim-html-%s/doc/html/spec_html/%sch%02d.html', $version, $prepend_chapter, $counter );
131                print "Generating  : docroot:/$path\n";
132                transform( $doc,
133                   "$opt{tmpl}/doc/chapter.xsl",
134                   "$opt{docroot}/$path",
135                );
136             }
137        }
138   }
139
140 ## Fixup xref tags
141   sub xref_fixup {
142      my( $xml, $prepend_chapter ) = @_;
143
144      my %index = ();
145
146      ## Add the "prepend_chapter" info
147        ($xml->findnodes('/book'))[0]->appendTextChild( 'prepend_chapter', $prepend_chapter );
148
149      ## Iterate over each chapter
150        my $chapter_counter = 0;
151        foreach my $chapter ( $xml->findnodes('/book/chapter') ){
152           ++$chapter_counter;
153
154           my $chapter_id    = $chapter->getAttribute('id');
155           my $chapter_title = $chapter->findvalue('title');
156
157           $index{$chapter_id} = { chapter_id => $chapter_counter, chapter_title => $chapter_title };
158
159           ## Iterate over each section
160             my $section_counter = 0;
161             foreach my $section ( $chapter->findnodes('section') ){
162                ++$section_counter;
163
164                my $section_id      = $section->getAttribute('id');
165                my $section_title   = $section->findvalue('title');
166
167                $index{$section_id} = { chapter_id => $chapter_counter, chapter_title => $chapter_title, section_id => $section_counter };
168             }
169        }
170
171     ## Replace all of the xrefs in the XML
172       foreach my $xref ( $xml->findnodes('//xref') ){
173          my $linkend = $xref->getAttribute('linkend');
174          if( exists $index{$linkend} ){
175              $xref->setAttribute( 'chapter_id',    $index{$linkend}{'chapter_id'}    );
176              $xref->setAttribute( 'chapter_title', $index{$linkend}{'chapter_title'} );
177              $xref->setAttribute( 'section_id',    $index{$linkend}{'section_id'}    ) if $index{$linkend}{'section_id'};
178              $xref->setAttribute( 'url', sprintf('%sch%02d.html',$prepend_chapter, $index{$linkend}{'chapter_id'}).($index{$linkend}{'section_id'}?'#'.$linkend:'') );
179           }
180        }
181   }
182
183 ## Handle the transformation
184   sub transform {
185      my( $xml, $xsl_path, $out_path ) = @_;
186
187      ## Build an empty XML structure if an undefined $xml was passed
188        unless( defined $xml ){
189           $xml = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
190           $xml->setDocumentElement( $xml->createElement('content') );
191        }
192
193      ## Add the current version of Exim to the XML
194        $xml->documentElement()->appendTextChild( 'current_version', $opt{latest} );
195
196      ## Parse the ".xsl" file as XML
197        my $xsl = XML::LibXML->new()->parse_file( $xsl_path ) or die $!;
198
199      ## Generate a stylesheet from the ".xsl" XML.
200        my $stylesheet = XML::LibXSLT->new()->parse_stylesheet( $xsl );
201
202      ## Generate a doc from the XML transformed with the XSL
203        my $doc = $stylesheet->transform( $xml );
204
205      ## Make the containing directory if it doesn't exist
206        mkdirp( ($out_path =~ /^(.+)\/.+$/)[0] );
207
208      ## Write out the document
209        open my $out, '>', $out_path or die $!;
210        print $out $stylesheet->output_as_bytes( $doc );
211        close $out;
212   }
213
214 ## "mkdir -p "
215   sub mkdirp {
216      my $path = shift;
217
218      my @parts = ();
219      foreach( split(/\//, $path) ){
220         push @parts, $_;
221         my $make = join('/',@parts);
222         next unless length($make);
223         next if -d $make;
224         mkdir( $make ) or die "Unable to mkdir $make: $!\n";
225      }
226   }
227
228 ## Parse arguments
229   sub parse_arguments {
230      my %opt = ();
231
232      ## --help
233        help(0) if int(@ARGV) == 0 || grep(/^--help|-h$/,@ARGV);
234
235      my @collection = @ARGV;
236      while( @collection ){
237         my $key = shift @collection;
238
239         if( $key eq '--web' ){
240
241            ## --web
242              $opt{web} = 1;
243         } elsif( $key =~ /^--(spec|filter)$/ ){
244
245            ## --spec and --filter
246              my $continue = 1;
247              while( $continue && @collection ){
248                 my $value = shift @collection;
249
250                 if( $value =~ /^--/ ){
251                    unshift @collection, $value;
252                    $continue = 0;
253                 } else {
254                    $value = File::Spec->rel2abs( $value );
255                    help( 1, 'No such file: '.$value   ) unless -f $value;
256                    push @{$opt{$1}}, $value unless grep( $_ eq $value, @{$opt{$1}} );
257                 }
258              }
259              help( 1, 'Missing value for '.$key ) unless exists $opt{$1};
260         } elsif( $key eq '--latest' ){
261
262            ## --latest
263              my $value = shift @collection;
264              help( 1, 'Missing value for '.$key ) unless defined $value;
265              help( 1, 'Invalid value for '.$key ) unless $value =~ /^\d+(?:\.\d+)*$/;
266              $opt{latest} = $value;
267         } elsif( $key =~ /^--(tmpl|docroot)$/ ){
268
269            ## --tmpl and --docroot
270              my $value = shift @collection;
271              help( 1, 'Missing value for '.$key    ) unless defined $value;
272              $value = File::Spec->rel2abs( $value );
273              help( 1, 'No such directory: '.$value ) unless -d $value;
274              $opt{$1} = $value;
275              $opt{$1} =~ s#/+$##;
276         } else {
277            help( 1, 'Bad argument: '.$key );
278         }
279      }
280
281      help( 1, 'Must include at least one of --web, --spec or --filter' ) unless exists $opt{web} || exists $opt{spec} || exists $opt{filter};
282      foreach(qw( latest tmpl docroot )){
283         help( 1, 'Missing argument: --'.$_ ) unless exists $opt{$_};
284      }
285
286      return %opt;
287   }
288
289 ## Help information
290   sub help {
291      my( $exit_code, $msg ) = @_;
292
293      print "$msg\n\n" if defined $msg;
294
295      print << "END_HELP";
296 Options:
297    --help or -h  : Print this help information and then exit
298
299    --web         : Generate the general website pages
300    --spec PATH   : Generate the spec pages. PATH is the path to the spec.xml
301    --filter PATH : Generate the filter pages. PATH is the path to the filter.xml
302
303    One or more of the above three options are required. --spec and --filter can
304    take multiple values to generate different sets of documentation for
305    different versions at the same time.
306
307    --latest VERSION : Required. Specify the latest stable version of Exim.
308    --tmpl PATH      : Required. Path to the templates directory
309    --docroot PATH   : Required. Path to the website document root
310
311    If CSS::Minifier::XS is installed, then CSS will be minified.
312    If JavaScript::Minifier::XS is installed, then JavaScript will be minified.
313
314 Example:
315
316    ./gen.pl --latest 4.72
317             --web
318             --spec spec.xml 4.71/spec.xml
319             --filter filter.xml 4.71/filter.xml
320             --tmpl ~/www/templates
321             --docroot ~/www/docroot
322 END_HELP
323
324      exit( $exit_code );
325   }