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 foreach my $chapter (@chapters) {
184 ## Add a <chapter_id>N</chapter_id> node for the stylesheet to use
185 $chapter->appendTextChild( 'chapter_id', ++$counter );
187 ## Add previous/next/canonical urls for nav
189 $chapter->appendTextChild( 'prev_url',
194 : sprintf( '%sch%02d.html', $prepend_chapter, $counter - 1 ) );
195 $chapter->appendTextChild( 'next_url', sprintf( '%sch%02d.html', $prepend_chapter, $counter + 1 ) )
196 unless int(@chapters) == $counter;
197 $chapter->appendTextChild( 'toc_url', ( $type eq 'filter' ? 'filter' : 'index' ) . '.html' );
198 $chapter->appendTextChild(
201 'http://www.exim.org/exim-html-current/doc/html/spec_html/%sch%02d.html',
202 $prepend_chapter, $counter
205 if ( $version ne $opt{latest} ) {
206 $chapter->appendTextChild(
209 '../../../../exim-html-current/doc/html/spec_html/%sch%02d.html',
210 $prepend_chapter, $counter
216 ## Create an XML document from the chapter
217 my $doc = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
218 $doc->setDocumentElement($chapter);
220 ## Transform the chapter into html
222 my $path = sprintf( 'exim-html-%s/doc/html/spec_html/%sch%02d.html', $version, $prepend_chapter, $counter );
223 print "Generating : docroot:/$path\n" if ( $opt{verbose} );
224 transform( $doc, "$opt{tmpl}/doc/chapter.xsl", "$opt{docroot}/$path", $staticroot );
229 # ------------------------------------------------------------------
232 my ( $xml, $prepend_chapter ) = @_;
236 ## Add the "prepend_chapter" info
237 ( $xml->findnodes('/book') )[0]->appendTextChild( 'prepend_chapter', $prepend_chapter );
239 ## Iterate over each chapter
240 my $chapter_counter = 0;
241 foreach my $chapter ( $xml->findnodes('/book/chapter') ) {
244 my $chapter_id = $chapter->getAttribute('id');
245 unless ($chapter_id) { # synthesise missing id
246 $chapter_id = sprintf( 'chapter_noid_%04d', $chapter_counter );
247 $chapter->setAttribute( 'id', $chapter_id );
249 my $chapter_title = $chapter->findvalue('title');
251 $index{$chapter_id} = { chapter_id => $chapter_counter, chapter_title => $chapter_title };
253 ## Iterate over each section
254 my $section_counter = 0;
255 foreach my $section ( $chapter->findnodes('section') ) {
258 my $section_id = $section->getAttribute('id');
259 unless ($section_id) { # synthesise missing id
260 $section_id = sprintf( 'section_noid_%04d_%04d', $chapter_counter, $section_counter );
261 $section->setAttribute( 'id', $section_id );
263 my $section_title = $section->findvalue('title');
265 $index{$section_id} = {
266 chapter_id => $chapter_counter,
267 chapter_title => $chapter_title,
268 section_id => $section_counter,
269 section_title => $section_title
273 ## Build indexes as new chapters
274 build_indexes( $xml, $prepend_chapter, \%index );
276 ## Replace all of the xrefs in the XML
277 foreach my $xref ( $xml->findnodes('//xref') ) {
278 my $linkend = $xref->getAttribute('linkend');
279 if ( exists $index{$linkend} ) {
280 $xref->setAttribute( 'chapter_id', $index{$linkend}{'chapter_id'} );
281 $xref->setAttribute( 'chapter_title', $index{$linkend}{'chapter_title'} );
282 $xref->setAttribute( 'section_id', $index{$linkend}{'section_id'} ) if ( $index{$linkend}{'section_id'} );
283 $xref->setAttribute( 'section_title', $index{$linkend}{'section_title'} )
284 if ( $index{$linkend}{'section_title'} );
285 $xref->setAttribute( 'url',
286 sprintf( '%sch%02d.html', $prepend_chapter, $index{$linkend}{'chapter_id'} )
287 . ( $index{$linkend}{'section_id'} ? '#' . $linkend : '' ) );
292 # ------------------------------------------------------------------
295 my ( $xml, $prepend_chapter, $xref ) = @_;
299 foreach my $node ( $xml->findnodes('//section | //chapter | //indexterm') ) {
300 if ( $node->nodeName eq 'indexterm' ) {
301 my $role = $node->getAttribute('role') || 'concept';
302 my $primary = $node->findvalue('child::primary');
303 my $first = ( $primary =~ /^[A-Za-z]/ ) ? uc( substr( $primary, 0, 1 ) ) : ''; # first letter or marker
304 my $secondary = $node->findvalue('child::secondary') || '';
305 next unless ( $primary || $secondary ); # skip blank entries for now...
306 $index_hash->{$role}{$first}{$primary}{$secondary} ||= [];
307 push @{ $index_hash->{$role}{$first}{$primary}{$secondary} }, $current_id;
310 $current_id = $node->getAttribute('id');
314 # now we build a set of new chapters with the index data in
315 my $book = ( $xml->findnodes('/book') )[0];
316 foreach my $role ( sort { $a cmp $b } keys %{$index_hash} ) {
317 my $chapter = XML::LibXML::Element->new('chapter');
318 $book->appendChild($chapter);
319 $chapter->setAttribute( 'id', join( '_', 'index', $role ) );
320 $chapter->setAttribute( 'class', 'index' );
321 $chapter->appendTextChild( 'title', ( ucfirst($role) . ' Index' ) );
322 foreach my $first ( sort { $a cmp $b } keys %{ $index_hash->{$role} } ) {
323 my $section = XML::LibXML::Element->new('section');
324 my $list = XML::LibXML::Element->new('variablelist');
325 $chapter->appendChild($section);
326 $section->setAttribute( 'id', join( '_', 'index', $role, $first ) );
327 $section->setAttribute( 'class', 'index' );
328 $section->appendTextChild( 'title', $first ? $first : 'Symbols' );
329 $section->appendChild($list);
330 foreach my $primary ( sort { $a cmp $b } keys %{ $index_hash->{$role}{$first} } ) {
331 my $entry = XML::LibXML::Element->new('varlistentry');
332 my $item = XML::LibXML::Element->new('listitem');
333 $list->appendChild($entry)->appendTextChild( 'term', $primary );
334 $entry->appendChild($item);
336 foreach my $secondary ( sort { $a cmp $b } keys %{ $index_hash->{$role}{$first}{$primary} } ) {
337 my $para = XML::LibXML::Element->new('para');
338 if ( $secondary eq '' ) {
339 $item->appendChild($para); # skip having extra layer of heirarchy
343 $slist = XML::LibXML::Element->new('variablelist');
344 $item->appendChild($slist);
346 my $sentry = XML::LibXML::Element->new('varlistentry');
347 my $sitem = XML::LibXML::Element->new('listitem');
348 $slist->appendChild($sentry)->appendTextChild( 'term', $secondary );
349 $sentry->appendChild($sitem)->appendChild($para);
352 foreach my $ref ( @{ $index_hash->{$role}{$first}{$primary}{$secondary} } ) {
353 $para->appendText(', ')
355 my $xrefel = XML::LibXML::Element->new('xref');
356 $xrefel->setAttribute( linkend => $ref );
357 $xrefel->setAttribute( longref => 1 );
358 $para->appendChild($xrefel);
366 # ------------------------------------------------------------------
367 ## Handle the transformation
369 my ( $xml, $xsl_path, $out_path, $staticroot_abs ) = @_;
371 ## make sure $staticroot is set
372 $staticroot_abs ||= $opt{staticroot};
374 ## Build an empty XML structure if an undefined $xml was passed
375 unless ( defined $xml ) {
376 $xml = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
377 $xml->setDocumentElement( $xml->createElement('content') );
380 ## Add the current version of Exim to the XML
381 $xml->documentElement()->appendTextChild( 'current_version', $opt{latest} );
383 ## Add the old versions of Exim to the XML
384 $xml->documentElement()->appendTextChild( 'old_versions', $_ ) foreach old_docs_versions();
386 ## Parse the ".xsl" file as XML
387 my $xsl = XML::LibXML->new()->parse_file($xsl_path) or die $!;
389 ## Generate a stylesheet from the ".xsl" XML.
390 my $stylesheet = XML::LibXSLT->new()->parse_stylesheet($xsl);
392 ## work out the static root relative to the target
393 my $target_dir = ( File::Spec->splitpath($out_path) )[1];
394 my $staticroot = File::Spec->abs2rel( $staticroot_abs, $target_dir );
396 ## Generate a doc from the XML transformed with the XSL
397 my $doc = $stylesheet->transform( $xml, staticroot => sprintf( "'%s'", $staticroot ) );
399 ## Make the containing directory if it doesn't exist
400 make_path( ( $out_path =~ /^(.+)\/.+$/ )[0], { verbose => $opt{verbose} } );
402 ## Write out the document
403 open my $out, '>', $out_path or die "Unable to write $out_path - $!";
404 print $out $stylesheet->output_as_bytes($doc);
408 # ------------------------------------------------------------------
409 ## Look in the docroot for old versions of the documentation
410 sub old_docs_versions {
411 if ( !exists $cache{old_docs_versions} ) {
413 foreach ( glob("$opt{docroot}/exim-html-*") ) {
414 push @versions, $1 if /-(\d+(?:\.\d+)?)$/ && $1 < $opt{latest} && -d $_;
416 $cache{old_docs_versions} = [ reverse sort { $a <=> $b } @versions ];
418 return @{ $cache{old_docs_versions} };
421 # ------------------------------------------------------------------
427 pod2usage( -exitval => 1, -verbose => 0 );
430 # ------------------------------------------------------------------
432 sub parse_arguments {
434 my %opt = ( spec => [], filter => [], help => 0, man => 0, web => 0, minify => 1, verbose => 0, localstatic => 0 );
436 \%opt, 'help|h!', 'man!', 'web!', 'spec=s{1,}', 'filter=s{1,}',
437 'latest=s', 'tmpl=s', 'docroot=s', 'minify!', 'verbose!', 'localstatic!'
438 ) || pod2usage( -exitval => 1, -verbose => 0 );
441 pod2usage(0) if ( $opt{help} );
442 pod2usage( -verbose => 2 ) if ( $opt{man} );
444 ## --spec and --filter lists
445 foreach my $set (qw[spec filter]) {
447 [ map { my $f = File::Spec->rel2abs($_); error_help( 1, 'No such file: ' . $_ ) unless -f $f; $f }
451 error_help('Missing value for latest') unless ( exists( $opt{latest} ) && defined( $opt{latest} ) );
452 error_help('Invalid value for latest') unless $opt{latest} =~ /^\d+(?:\.\d+)*$/;
454 ## --tmpl and --docroot
455 foreach my $set (qw[tmpl docroot]) {
456 error_help( 'Missing value for ' . $set ) unless ( exists( $opt{$set} ) && defined( $opt{$set} ) );
457 my $f = File::Spec->rel2abs( $opt{$set} );
458 error_help( 'No such directory: ' . $opt{$set} ) unless -d $f;
461 error_help('Excess arguments') if ( scalar(@ARGV) );
463 error_help('Must include at least one of --web, --spec or --filter')
464 unless ( $opt{web} || scalar( @{ $opt{spec} || [] } ) || scalar( @{ $opt{filter} || [] } ) );
469 # ------------------------------------------------------------------
476 gen.pl - Generate exim html documentation and website
483 --help display this help and exits
484 --man displays man page
485 --spec file... spec docbook/XML source files
486 --filter file... filter docbook/XML source files
487 --web Generate the general website pages
488 --latest VERSION Required. Specify the latest stable version of Exim.
489 --tmpl PATH Required. Path to the templates directory
490 --docroot PATH Required. Path to the website document root
491 --[no-]minify [Don't] minify CSS and Javascript
492 --localstatic Makes the static files local to each doc ver
500 Display help and exits
506 =item B<--spec> I<file...>
508 List of files that make up the specification documentation
509 docbook/XML source files.
511 =item B<--filter> I<file...>
513 List of files that make up the filter documentation docbook/XML
518 Generate the website from the template files.
520 =item B<--latest> I<version>
522 Specify the current exim version. This is used to create links to
523 the current documentation.
525 This option is I<required>
527 =item B<--tmpl> I<directory>
529 Specify the directory that the templates are kept in.
531 This option is I<required>
533 =item B<--docroot> I<directory>
535 Specify the directory that the output should be generated into.
536 This is the website C<docroot> directory.
538 This option is I<required>
542 If this option is set then both the CSS and Javascript files
543 processed are minified using L<CSS::Minifier::XS> and
544 L<JavaScript::Minifier::XS> respectively.
546 This option is set by default - to disable it specify C<--no-minify>
548 =item B<--localstatic>
550 Makes the static files (CSS, images etc), local for each version of
551 the documentation. This is more suitable for packaged HTML
558 Generates the exim website and HTML documentation.
564 --spec docbook/*/spec.xml \
565 --filter docbook/*/filter.xml \
568 --docroot /tmp/website
574 Nigel Metheringham <nigel@exim.org> - mostly broke the framework
579 Copyright 2010-2012 Exim Maintainers. All rights reserved.