6 use CSS::Minifier::XS 0.07;
9 use File::Path qw(make_path);
13 use JavaScript::Minifier::XS;
18 my $canonical_url = 'http://www.exim.org/';
21 my %opt = parse_arguments();
23 ## setup static root location
24 ## TODO: for doc generation only this should be within the docs dir
25 $opt{staticroot} = File::Spec->catdir( $opt{docroot}, 'static' );
28 my %cache; # General cache object
29 do_doc( 'spec', $_ ) foreach @{ $opt{spec} || [] };
30 do_doc( 'filter', $_ ) foreach @{ $opt{filter} || [] };
31 do_web() if ( $opt{web} );
32 do_static() if ( $opt{web} or !$opt{localstatic} ); # need this for all other pages generated
34 ## Add the exim-html-current symlink
35 print "Symlinking exim-html-current to exim-html-$opt{latest}\n" if ( $opt{verbose} );
36 unlink("$opt{docroot}/exim-html-current") if ( -l "$opt{docroot}/exim-html-current" );
37 symlink( "exim-html-$opt{latest}", "$opt{docroot}/exim-html-current" )
38 || die "symlink to $opt{docroot}/exim-html-current failed";
40 # ------------------------------------------------------------------
41 ## Generate the website files
44 ## copy these templates to docroot...
45 copy_transform_files( "$opt{tmpl}/web", $opt{docroot}, 0 );
48 # ------------------------------------------------------------------
49 ## Generate the static file set
51 my $staticroot = shift || $opt{staticroot};
53 ## make sure I have a directory
54 mkdir($staticroot) or die "Unable to make staticroot: $!\n" unless ( -d $staticroot );
56 ## copy these templates to docroot...
57 copy_transform_files( "$opt{tmpl}/static", $staticroot, 1 );
60 # ------------------------------------------------------------------
61 ## Generate the website files
62 sub copy_transform_files {
67 ## Make sure the template web directory exists
68 die "No such directory: $source\n" unless ( -d $source );
70 ## Scan the web templates
73 my ($path) = substr( $File::Find::name, length("$source"), length($File::Find::name) ) =~ m#^/*(.*)$#;
75 if ( -d "$source/$path" ) {
77 ## Create the directory in the target if it doesn't exist
78 if ( !-d "$target/$path" ) {
79 mkdir("$target/$path") or die "Unable to make $target/$path: $!\n";
85 ## Build HTML from XSL files and simply copy static files which have changed
86 if ( ( !$static ) and ( $path =~ /(.+)\.xsl$/ ) ) {
87 print "Generating : /$1.html\n" if ( $opt{verbose} );
88 transform( undef, "$source/$path", "$target/$1.html" );
90 elsif ( -f "$source/$path" ) {
92 ## Skip if the file hasn't changed (mtime/size based)
94 if (( -f "$target/$path" )
95 and ( ( stat("$source/$path") )[9] == ( stat("$target/$path") )[9] )
96 and ( ( stat("$source/$path") )[7] == ( stat("$target/$path") )[7] ) );
98 if ( $path =~ /(.+)\.css$/ ) {
99 print "CSS to : /$path\n" if ( $opt{verbose} );
100 my $content = read_file("$source/$path");
101 write_file( "$target/$path", $opt{minify} ? CSS::Minifier::XS::minify($content) : $content );
103 elsif ( $path =~ /(.+)\.js$/ ) {
104 print "JS to : /$path\n" if ( $opt{verbose} );
105 my $content = read_file("$source/$path");
106 write_file( "$target/$path",
107 $opt{minify} ? JavaScript::Minifier::XS::minify($content) : $content );
111 print "Copying to : /$path\n" if ( $opt{verbose} );
112 copy( "$source/$path", "$target/$path" ) or die "$path: $!";
115 utime( time, ( stat("$source/$path") )[9], "$target/$path" );
124 # ------------------------------------------------------------------
125 ## Generate index/chapter files for a doc
127 my ( $type, $xml_path ) = @_;
129 ## Read and validate the XML file
130 my $xml = XML::LibXML->new()->parse_file($xml_path) or die $!;
132 ## Get the version number
133 my $version = $xml->findvalue('/book/bookinfo/revhistory/revision/revnumber');
134 die "Unable to get version number\n" unless defined $version && $version =~ /^\d+(\.\d+)*$/;
136 ## Prepend chapter filenames?
137 my $prepend_chapter = $type eq 'filter' ? 'filter_' : '';
139 ## Add the canonical url for this document
140 $xml->documentElement()
141 ->appendTextChild( 'canonical_url',
142 "${canonical_url}exim-html-current/doc/html/spec_html/" . ( $type eq 'spec' ? 'index' : 'filter' ) . ".html" );
144 ## Add a url for the latest version of this document
145 if ( $version ne $opt{latest} ) {
146 $xml->documentElement()
147 ->appendTextChild( 'current_url',
148 "../../../../exim-html-current/doc/html/spec_html/" . ( $type eq 'spec' ? 'index' : 'filter' ) . ".html" );
152 xref_fixup( $xml, $prepend_chapter );
154 ## set the staticroot
157 ? File::Spec->catdir( $opt{docroot}, "exim-html-$version", 'doc', 'html', 'static' )
159 unless ( -d $staticroot ) {
160 make_path( $staticroot, { verbose => $opt{verbose} } );
161 do_static($staticroot);
164 ## Generate the front page
166 my $path = "exim-html-$version/doc/html/spec_html/" . ( $type eq 'filter' ? $type : 'index' ) . ".html";
167 print "Generating : docroot:/$path\n" if ( $opt{verbose} );
168 transform( $xml, "$opt{tmpl}/doc/index.xsl", "$opt{docroot}/$path", $staticroot );
171 ## Generate a Table of Contents XML file
174 "exim-html-$version/doc/html/spec_html/" . ( $type eq 'filter' ? 'filter_toc' : 'index_toc' ) . ".xml";
175 print "Generating : docroot:/$path\n" if ( $opt{verbose} );
176 transform( $xml, "$opt{tmpl}/doc/toc.xsl", "$opt{docroot}/$path", $staticroot );
179 ## Generate the chapters
181 my @chapters = map { $_->cloneNode(1) } $xml->findnodes('/book/chapter');
182 my( $chapter_title, $chapter_title_prev, $chapter_title_next );
183 foreach my $chapter (@chapters) {
185 ## Add a <chapter_id>N</chapter_id> node for the stylesheet to use
186 $chapter->appendTextChild( 'chapter_id', ++$counter );
188 ## Get the current and surrounding chapter titles
189 $chapter_title_prev = $chapter_title;
190 $chapter_title = $chapter_title_next || $chapter->findvalue('title_uri');
191 $chapter_title_next = $chapters[$counter]->findvalue('title_uri') if $counter < int(@chapters);
193 ## Add previous/next/canonical urls for nav
195 $chapter->appendTextChild( 'prev_url',
200 : sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title_prev ) );
201 $chapter->appendTextChild( 'this_url', sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title ) );
202 $chapter->appendTextChild( 'next_url', sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title_next ) )
203 unless int(@chapters) == $counter;
204 $chapter->appendTextChild( 'toc_url', ( $type eq 'filter' ? 'filter' : 'index' ) . '.html' );
205 $chapter->appendTextChild(
208 'http://www.exim.org/exim-html-current/doc/html/spec_html/%sch-%s.html',
209 $prepend_chapter, $chapter_title
212 if ( $version ne $opt{latest} ) {
213 $chapter->appendTextChild(
216 '../../../../exim-html-current/doc/html/spec_html/%sch-%s.html',
217 $prepend_chapter, $chapter_title
223 ## Create an XML document from the chapter
224 my $doc = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
225 $doc->setDocumentElement($chapter);
227 ## Transform the chapter into html
229 my $real_path = sprintf( 'exim-html-%s/doc/html/spec_html/%sch-%s.html', $version, $prepend_chapter, $chapter_title );
230 my $link_path = sprintf( 'exim-html-%s/doc/html/spec_html/%sch%02d.html', $version, $prepend_chapter, $counter );
231 print "Generating : docroot:/$real_path\n" if ( $opt{verbose} );
232 transform( $doc, "$opt{tmpl}/doc/chapter.xsl", "$opt{docroot}/$real_path", $staticroot );
233 print "Symlinking : docroot:/$link_path to docroot:$real_path\n" if ( $opt{verbose} );
234 if ( -f "$opt{docroot}/$link_path" ) {
235 unlink("$opt{docroot}/$link_path") or die "failed removing $opt{docroot}/$link_path: $!";
237 symlink( "$opt{docroot}/$real_path", "$opt{docroot}/$link_path" ) || die "symlink to $opt{docroot}/$link_path failed: $!";
242 # ------------------------------------------------------------------
245 my ( $xml, $prepend_chapter ) = @_;
249 ## Add the "prepend_chapter" info
250 ( $xml->findnodes('/book') )[0]->appendTextChild( 'prepend_chapter', $prepend_chapter );
252 ## Iterate over each chapter
253 my $chapter_counter = 0;
254 foreach my $chapter ( $xml->findnodes('/book/chapter') ) {
257 my $chapter_id = $chapter->getAttribute('id');
258 unless ($chapter_id) { # synthesise missing id
259 $chapter_id = sprintf( 'chapter_noid_%04d', $chapter_counter );
260 $chapter->setAttribute( 'id', $chapter_id );
262 my $chapter_title = $chapter->findvalue('title');
264 ## Set title_uri so we can use eg ch-introduction.html instead of ch01.html
265 $chapter->appendTextChild( 'title_uri', title_to_uri($chapter_title) );
267 $index{$chapter_id} = { chapter_id => $chapter_counter, chapter_title => $chapter_title };
269 ## Iterate over each section
270 my $section_counter = 0;
271 foreach my $section ( $chapter->findnodes('section') ) {
274 my $section_id = $section->getAttribute('id');
275 unless ($section_id) { # synthesise missing id
276 $section_id = sprintf( 'section_noid_%04d_%04d', $chapter_counter, $section_counter );
277 $section->setAttribute( 'id', $section_id );
279 my $section_title = $section->findvalue('title');
281 $index{$section_id} = {
282 chapter_id => $chapter_counter,
283 chapter_title => $chapter_title,
284 section_id => $section_counter,
285 section_title => $section_title
289 ## Build indexes as new chapters
290 build_indexes( $xml, $prepend_chapter, \%index );
292 ## Replace all of the xrefs in the XML
293 foreach my $xref ( $xml->findnodes('//xref') ) {
294 my $linkend = $xref->getAttribute('linkend');
295 if ( exists $index{$linkend} ) {
296 $xref->setAttribute( 'chapter_id', $index{$linkend}{'chapter_id'} );
297 $xref->setAttribute( 'chapter_title', $index{$linkend}{'chapter_title'} );
298 $xref->setAttribute( 'section_id', $index{$linkend}{'section_id'} ) if ( $index{$linkend}{'section_id'} );
299 $xref->setAttribute( 'section_title', $index{$linkend}{'section_title'} )
300 if ( $index{$linkend}{'section_title'} );
301 $xref->setAttribute( 'url',
302 sprintf( '%sch-%s.html', $prepend_chapter, title_to_uri($index{$linkend}{'chapter_title'}) )
303 . ( $index{$linkend}{'section_id'} ? '#' . $linkend : '' ) );
308 # ------------------------------------------------------------------
311 my ( $xml, $prepend_chapter, $xref ) = @_;
315 foreach my $node ( $xml->findnodes('//section | //chapter | //indexterm') ) {
316 if ( $node->nodeName eq 'indexterm' ) {
317 my $role = $node->getAttribute('role') || 'concept';
318 my $primary = $node->findvalue('child::primary');
319 my $first = ( $primary =~ /^[A-Za-z]/ ) ? uc( substr( $primary, 0, 1 ) ) : ''; # first letter or marker
320 my $secondary = $node->findvalue('child::secondary') || '';
321 next unless ( $primary || $secondary ); # skip blank entries for now...
322 $index_hash->{$role}{$first}{$primary}{$secondary} ||= [];
323 push @{ $index_hash->{$role}{$first}{$primary}{$secondary} }, $current_id;
326 $current_id = $node->getAttribute('id');
330 # now we build a set of new chapters with the index data in
331 my $book = ( $xml->findnodes('/book') )[0];
332 foreach my $role ( sort { $a cmp $b } keys %{$index_hash} ) {
333 my $chapter = XML::LibXML::Element->new('chapter');
334 $book->appendChild($chapter);
335 $chapter->setAttribute( 'id', join( '_', 'index', $role ) );
336 $chapter->setAttribute( 'class', 'index' );
337 $chapter->appendTextChild( 'title', ( ucfirst($role) . ' Index' ) );
338 $chapter->appendTextChild( 'title_uri', title_to_uri(ucfirst($role) . ' Index') );
340 foreach my $first ( sort { $a cmp $b } keys %{ $index_hash->{$role} } ) {
341 my $section = XML::LibXML::Element->new('section');
342 my $list = XML::LibXML::Element->new('variablelist');
343 $chapter->appendChild($section);
344 $section->setAttribute( 'id', join( '_', 'index', $role, $first ) );
345 $section->setAttribute( 'class', 'index' );
346 $section->appendTextChild( 'title', $first ? $first : 'Symbols' );
347 $section->appendChild($list);
348 foreach my $primary ( sort { $a cmp $b } keys %{ $index_hash->{$role}{$first} } ) {
349 my $entry = XML::LibXML::Element->new('varlistentry');
350 my $item = XML::LibXML::Element->new('listitem');
351 $list->appendChild($entry)->appendTextChild( 'term', $primary );
352 $entry->appendChild($item);
354 foreach my $secondary ( sort { $a cmp $b } keys %{ $index_hash->{$role}{$first}{$primary} } ) {
355 my $para = XML::LibXML::Element->new('para');
356 if ( $secondary eq '' ) {
357 $item->appendChild($para); # skip having extra layer of heirarchy
361 $slist = XML::LibXML::Element->new('variablelist');
362 $item->appendChild($slist);
364 my $sentry = XML::LibXML::Element->new('varlistentry');
365 my $sitem = XML::LibXML::Element->new('listitem');
366 $slist->appendChild($sentry)->appendTextChild( 'term', $secondary );
367 $sentry->appendChild($sitem)->appendChild($para);
370 foreach my $ref ( @{ $index_hash->{$role}{$first}{$primary}{$secondary} } ) {
371 $para->appendText(', ')
373 my $xrefel = XML::LibXML::Element->new('xref');
374 $xrefel->setAttribute( linkend => $ref );
375 $xrefel->setAttribute( longref => 1 );
376 $para->appendChild($xrefel);
384 # ------------------------------------------------------------------
385 ## Handle the transformation
387 my ( $xml, $xsl_path, $out_path, $staticroot_abs ) = @_;
389 ## make sure $staticroot is set
390 $staticroot_abs ||= $opt{staticroot};
392 ## Build an empty XML structure if an undefined $xml was passed
393 unless ( defined $xml ) {
394 $xml = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
395 $xml->setDocumentElement( $xml->createElement('content') );
398 ## Add the current version of Exim to the XML
399 $xml->documentElement()->appendTextChild( 'current_version', $opt{latest} );
401 ## Add the old versions of Exim to the XML
402 $xml->documentElement()->appendTextChild( 'old_versions', $_ ) foreach old_docs_versions();
404 ## Parse the ".xsl" file as XML
405 my $xsl = XML::LibXML->new()->parse_file($xsl_path) or die $!;
407 ## Generate a stylesheet from the ".xsl" XML.
408 my $stylesheet = XML::LibXSLT->new()->parse_stylesheet($xsl);
410 ## work out the static root relative to the target
411 my $target_dir = ( File::Spec->splitpath($out_path) )[1];
412 my $staticroot = File::Spec->abs2rel( $staticroot_abs, $target_dir );
414 ## Generate a doc from the XML transformed with the XSL
415 my $doc = $stylesheet->transform( $xml, staticroot => sprintf( "'%s'", $staticroot ) );
417 ## Make the containing directory if it doesn't exist
418 make_path( ( $out_path =~ /^(.+)\/.+$/ )[0], { verbose => $opt{verbose} } );
420 ## Write out the document
421 open my $out, '>', $out_path or die "Unable to write $out_path - $!";
422 print $out $stylesheet->output_as_bytes($doc);
426 # ------------------------------------------------------------------
427 ## Takes a chapter title and fixes it up so it is suitable for use in a URI
429 my $title = lc(shift);
430 $title =~ s/[^a-z0-9\s]+//gi; # Only allow spaces, numbers and letters
431 $title =~ s/\s+/_/g; # Replace spaces with underscores so URLs are easier to copy about
435 # ------------------------------------------------------------------
436 ## Look in the docroot for old versions of the documentation
437 sub old_docs_versions {
438 if ( !exists $cache{old_docs_versions} ) {
440 foreach ( glob("$opt{docroot}/exim-html-*") ) {
441 push @versions, $1 if /-(\d+(?:\.\d+)?)$/ && $1 lt $opt{latest} && -d $_;
443 $cache{old_docs_versions} = [ reverse sort { $a cmp $b } @versions ];
445 return @{ $cache{old_docs_versions} };
448 # ------------------------------------------------------------------
454 pod2usage( -exitval => 1, -verbose => 0 );
457 # ------------------------------------------------------------------
459 sub parse_arguments {
461 my %opt = ( spec => [], filter => [], help => 0, man => 0, web => 0, minify => 1, verbose => 0, localstatic => 0 );
463 \%opt, 'help|h!', 'man!', 'web!', 'spec=s{1,}', 'filter=s{1,}',
464 'latest=s', 'tmpl=s', 'docroot=s', 'minify!', 'verbose!', 'localstatic!'
465 ) || pod2usage( -exitval => 1, -verbose => 0 );
468 pod2usage(0) if ( $opt{help} );
469 pod2usage( -verbose => 2 ) if ( $opt{man} );
471 ## --spec and --filter lists
472 foreach my $set (qw[spec filter]) {
474 [ map { my $f = File::Spec->rel2abs($_); error_help( 1, 'No such file: ' . $_ ) unless -f $f; $f }
478 error_help('Missing value for latest') unless ( exists( $opt{latest} ) && defined( $opt{latest} ) );
479 error_help('Invalid value for latest') unless $opt{latest} =~ /^\d+(?:\.\d+)*$/;
481 ## --tmpl and --docroot
482 foreach my $set (qw[tmpl docroot]) {
483 error_help( 'Missing value for ' . $set ) unless ( exists( $opt{$set} ) && defined( $opt{$set} ) );
484 my $f = File::Spec->rel2abs( $opt{$set} );
485 error_help( 'No such directory: ' . $opt{$set} ) unless -d $f;
488 error_help('Excess arguments') if ( scalar(@ARGV) );
490 error_help('Must include at least one of --web, --spec or --filter')
491 unless ( $opt{web} || scalar( @{ $opt{spec} || [] } ) || scalar( @{ $opt{filter} || [] } ) );
496 # ------------------------------------------------------------------
503 gen.pl - Generate exim html documentation and website
510 --help display this help and exits
511 --man displays man page
512 --spec file... spec docbook/XML source files
513 --filter file... filter docbook/XML source files
514 --web Generate the general website pages
515 --latest VERSION Required. Specify the latest stable version of Exim.
516 --tmpl PATH Required. Path to the templates directory
517 --docroot PATH Required. Path to the website document root
518 --[no-]minify [Don't] minify CSS and Javascript
519 --localstatic Makes the static files local to each doc ver
527 Display help and exits
533 =item B<--spec> I<file...>
535 List of files that make up the specification documentation docbook/XML source
538 =item B<--filter> I<file...>
540 List of files that make up the filter documentation docbook/XML source files.
544 Generate the website from the template files.
546 =item B<--latest> I<version>
548 Specify the current exim version. This is used to create links to the current
551 This option is I<required>
553 =item B<--tmpl> I<directory>
555 Specify the directory that the templates are kept in.
557 This option is I<required>
559 =item B<--docroot> I<directory>
561 Specify the directory that the output should be generated into. This is the
562 website C<docroot> directory.
564 This option is I<required>
568 If this option is set then both the CSS and Javascript files processed are
569 minified using L<CSS::Minifier::XS> and L<JavaScript::Minifier::XS>
572 This option is set by default - to disable it specify C<--no-minify>
574 =item B<--localstatic>
576 Makes the static files (CSS, images etc), local for each version of the
577 documentation. This is more suitable for packaged HTML documentation.
583 Generates the exim website and HTML documentation.
589 --spec docbook/*/spec.xml \
590 --filter docbook/*/filter.xml \
593 --docroot /tmp/website
599 Nigel Metheringham <nigel@exim.org> - mostly broke the framework Mike produced.
603 Copyright 2010-2012 Exim Maintainers. All rights reserved.