6 use CSS::Minifier::XS 0.07;
10 use File::Path qw(make_path);
14 use JavaScript::Minifier::XS;
19 my $canonical_url = 'http://www.exim.org/';
22 my %opt = parse_arguments();
24 ## setup static root location
25 ## TODO: for doc generation only this should be within the docs dir
26 $opt{staticroot} = File::Spec->catdir( $opt{docroot}, 'static' );
29 my %cache; # General cache object
30 do_doc( 'spec', $_ ) foreach @{ $opt{spec} || [] };
31 do_doc( 'filter', $_ ) foreach @{ $opt{filter} || [] };
32 do_web() if ( $opt{web} );
33 do_static() if ( $opt{web} or !$opt{localstatic} ); # need this for all other pages generated
35 ## Add the exim-html-current symlink
36 print "Symlinking exim-html-current to exim-html-$opt{latest}\n" if ( $opt{verbose} );
37 unlink("$opt{docroot}/exim-html-current") if ( -l "$opt{docroot}/exim-html-current" );
38 symlink( "exim-html-$opt{latest}", "$opt{docroot}/exim-html-current" )
39 || die "symlink to $opt{docroot}/exim-html-current failed";
41 # ------------------------------------------------------------------
42 ## Generate the website files
45 ## copy these templates to docroot...
46 copy_transform_files( "$opt{tmpl}/web", $opt{docroot}, 0 );
49 # ------------------------------------------------------------------
50 ## Generate the static file set
52 my $staticroot = shift || $opt{staticroot};
54 ## make sure I have a directory
55 mkdir($staticroot) or die "Unable to make staticroot: $!\n" unless ( -d $staticroot );
57 ## copy these templates to docroot...
58 copy_transform_files( "$opt{tmpl}/static", $staticroot, 1 );
61 # ------------------------------------------------------------------
62 ## Generate the website files
63 sub copy_transform_files {
68 ## Make sure the template web directory exists
69 die "No such directory: $source\n" unless ( -d $source );
71 ## Scan the web templates
74 my ($path) = substr( $File::Find::name, length("$source"), length($File::Find::name) ) =~ m#^/*(.*)$#;
76 if ( -d "$source/$path" ) {
78 ## Create the directory in the target if it doesn't exist
79 if ( !-d "$target/$path" ) {
80 mkdir("$target/$path") or die "Unable to make $target/$path: $!\n";
86 ## Build HTML from XSL files and simply copy static files which have changed
87 if ( ( !$static ) and ( $path =~ /(.+)\.xsl$/ ) ) {
88 print "Generating : /$1.html\n" if ( $opt{verbose} );
89 transform( undef, "$source/$path", "$target/$1.html" );
91 elsif ( -f "$source/$path" ) {
93 ## Skip if the file hasn't changed (mtime/size based)
95 if (( -f "$target/$path" )
96 and ( ( stat("$source/$path") )[9] == ( stat("$target/$path") )[9] )
97 and ( ( stat("$source/$path") )[7] == ( stat("$target/$path") )[7] ) );
99 if ( $path =~ /(.+)\.css$/ ) {
100 print "CSS to : /$path\n" if ( $opt{verbose} );
101 my $content = read_file("$source/$path");
102 write_file( "$target/$path", $opt{minify} ? CSS::Minifier::XS::minify($content) : $content );
104 elsif ( $path =~ /(.+)\.js$/ ) {
105 print "JS to : /$path\n" if ( $opt{verbose} );
106 my $content = read_file("$source/$path");
107 write_file( "$target/$path",
108 $opt{minify} ? JavaScript::Minifier::XS::minify($content) : $content );
112 print "Copying to : /$path\n" if ( $opt{verbose} );
113 copy( "$source/$path", "$target/$path" ) or die "$path: $!";
116 utime( time, ( stat("$source/$path") )[9], "$target/$path" );
125 # ------------------------------------------------------------------
126 ## Generate index/chapter files for a doc
128 my ( $type, $xml_path ) = @_;
130 ## Read and validate the XML file
131 my $xml = XML::LibXML->new()->parse_file($xml_path) or die $!;
133 ## Get the version number
134 my $version = $xml->findvalue('/book/bookinfo/revhistory/revision/revnumber');
135 die "Unable to get version number\n" unless defined $version && $version =~ /^\d+(\.\d+)*$/;
137 ## Prepend chapter filenames?
138 my $prepend_chapter = $type eq 'filter' ? 'filter_' : '';
140 ## Add the canonical url for this document
141 $xml->documentElement()
142 ->appendTextChild( 'canonical_url',
143 "${canonical_url}exim-html-current/doc/html/spec_html/" . ( $type eq 'spec' ? 'index' : 'filter' ) . ".html" );
145 ## Add a url for the latest version of this document
146 if ( $version ne $opt{latest} ) {
147 $xml->documentElement()
148 ->appendTextChild( 'current_url',
149 "../../../../exim-html-current/doc/html/spec_html/" . ( $type eq 'spec' ? 'index' : 'filter' ) . ".html" );
153 xref_fixup( $xml, $prepend_chapter );
155 ## set the staticroot
158 ? File::Spec->catdir( $opt{docroot}, "exim-html-$version", 'doc', 'html', 'static' )
160 unless ( -d $staticroot ) {
161 make_path( $staticroot, { verbose => $opt{verbose} } );
162 do_static($staticroot);
165 ## Generate the front page
167 my $path = "exim-html-$version/doc/html/spec_html/" . ( $type eq 'filter' ? $type : 'index' ) . ".html";
168 print "Generating : docroot:/$path\n" if ( $opt{verbose} );
169 transform( $xml, "$opt{tmpl}/doc/index.xsl", "$opt{docroot}/$path", $staticroot );
172 ## Generate a Table of Contents XML file
175 "exim-html-$version/doc/html/spec_html/" . ( $type eq 'filter' ? 'filter_toc' : 'index_toc' ) . ".xml";
176 print "Generating : docroot:/$path\n" if ( $opt{verbose} );
177 transform( $xml, "$opt{tmpl}/doc/toc.xsl", "$opt{docroot}/$path", $staticroot );
180 ## Generate the chapters
182 my @chapters = map { $_->cloneNode(1) } $xml->findnodes('/book/chapter');
183 my( $chapter_title, $chapter_title_prev, $chapter_title_next );
184 foreach my $chapter (@chapters) {
186 ## Add a <chapter_id>N</chapter_id> node for the stylesheet to use
187 $chapter->appendTextChild( 'chapter_id', ++$counter );
189 ## Get the current and surrounding chapter titles
190 $chapter_title_prev = $chapter_title;
191 $chapter_title = $chapter_title_next || $chapter->findvalue('title_uri');
192 $chapter_title_next = $chapters[$counter]->findvalue('title_uri') if $counter < int(@chapters);
194 ## Add previous/next/canonical urls for nav
196 $chapter->appendTextChild( 'prev_url',
201 : sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title_prev ) );
202 $chapter->appendTextChild( 'this_url', sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title ) );
203 $chapter->appendTextChild( 'next_url', sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title_next ) )
204 unless int(@chapters) == $counter;
205 $chapter->appendTextChild( 'toc_url', ( $type eq 'filter' ? 'filter' : 'index' ) . '.html' );
206 $chapter->appendTextChild(
209 'http://www.exim.org/exim-html-current/doc/html/spec_html/%sch-%s.html',
210 $prepend_chapter, $chapter_title
213 if ( $version ne $opt{latest} ) {
214 $chapter->appendTextChild(
217 '../../../../exim-html-current/doc/html/spec_html/%sch-%s.html',
218 $prepend_chapter, $chapter_title
224 ## Create an XML document from the chapter
225 my $doc = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
226 $doc->setDocumentElement($chapter);
228 ## Transform the chapter into html
230 my $real_path = sprintf( 'exim-html-%s/doc/html/spec_html/%sch-%s.html', $version, $prepend_chapter, $chapter_title );
231 my $link_path = sprintf( 'exim-html-%s/doc/html/spec_html/%sch%02d.html', $version, $prepend_chapter, $counter );
232 print "Generating : docroot:/$real_path\n" if ( $opt{verbose} );
233 transform( $doc, "$opt{tmpl}/doc/chapter.xsl", "$opt{docroot}/$real_path", $staticroot );
234 # Making a relative symlink to a file in the same directory.
235 # Extract just the filename portion of $real_path.
236 my $real_file = basename($real_path);
237 print "Symlinking : docroot:/$link_path to $real_file\n" if ( $opt{verbose} );
238 if ( -f "$opt{docroot}/$link_path" ) {
239 unlink("$opt{docroot}/$link_path") or die "failed removing $opt{docroot}/$link_path: $!";
241 symlink( "$real_file", "$opt{docroot}/$link_path" ) || die "symlink to $opt{docroot}/$link_path failed: $!";
246 # ------------------------------------------------------------------
249 my ( $xml, $prepend_chapter ) = @_;
253 ## Add the "prepend_chapter" info
254 ( $xml->findnodes('/book') )[0]->appendTextChild( 'prepend_chapter', $prepend_chapter );
256 ## Iterate over each chapter
257 my $chapter_counter = 0;
258 foreach my $chapter ( $xml->findnodes('/book/chapter') ) {
261 my $chapter_id = $chapter->getAttribute('id');
262 unless ($chapter_id) { # synthesise missing id
263 $chapter_id = sprintf( 'chapter_noid_%04d', $chapter_counter );
264 $chapter->setAttribute( 'id', $chapter_id );
266 my $chapter_title = $chapter->findvalue('title');
268 ## Set title_uri so we can use eg ch-introduction.html instead of ch01.html
269 $chapter->appendTextChild( 'title_uri', title_to_uri($chapter_title) );
271 $index{$chapter_id} = { chapter_id => $chapter_counter, chapter_title => $chapter_title };
273 ## Iterate over each section
274 my $section_counter = 0;
275 foreach my $section ( $chapter->findnodes('section') ) {
278 my $section_id = $section->getAttribute('id');
279 unless ($section_id) { # synthesise missing id
280 $section_id = sprintf( 'section_noid_%04d_%04d', $chapter_counter, $section_counter );
281 $section->setAttribute( 'id', $section_id );
283 my $section_title = $section->findvalue('title');
285 $index{$section_id} = {
286 chapter_id => $chapter_counter,
287 chapter_title => $chapter_title,
288 section_id => $section_counter,
289 section_title => $section_title
293 ## Build indexes as new chapters
294 build_indexes( $xml, $prepend_chapter, \%index );
296 ## Replace all of the xrefs in the XML
297 foreach my $xref ( $xml->findnodes('//xref') ) {
298 my $linkend = $xref->getAttribute('linkend');
299 if ( exists $index{$linkend} ) {
300 $xref->setAttribute( 'chapter_id', $index{$linkend}{'chapter_id'} );
301 $xref->setAttribute( 'chapter_title', $index{$linkend}{'chapter_title'} );
302 $xref->setAttribute( 'section_id', $index{$linkend}{'section_id'} ) if ( $index{$linkend}{'section_id'} );
303 $xref->setAttribute( 'section_title', $index{$linkend}{'section_title'} )
304 if ( $index{$linkend}{'section_title'} );
305 $xref->setAttribute( 'url',
306 sprintf( '%sch-%s.html', $prepend_chapter, title_to_uri($index{$linkend}{'chapter_title'}) )
307 . ( $index{$linkend}{'section_id'} ? '#' . $linkend : '' ) );
312 # ------------------------------------------------------------------
315 my ( $xml, $prepend_chapter, $xref ) = @_;
319 foreach my $node ( $xml->findnodes('//section | //chapter | //indexterm') ) {
320 if ( $node->nodeName eq 'indexterm' ) {
321 my $role = $node->getAttribute('role') || 'concept';
322 my $primary = $node->findvalue('child::primary');
323 my $first = ( $primary =~ /^[A-Za-z]/ ) ? uc( substr( $primary, 0, 1 ) ) : ''; # first letter or marker
324 my $secondary = $node->findvalue('child::secondary') || '';
325 next unless ( $primary || $secondary ); # skip blank entries for now...
326 $index_hash->{$role}{$first}{$primary}{$secondary} ||= [];
327 push @{ $index_hash->{$role}{$first}{$primary}{$secondary} }, $current_id;
330 $current_id = $node->getAttribute('id');
334 # now we build a set of new chapters with the index data in
335 my $book = ( $xml->findnodes('/book') )[0];
336 foreach my $role ( sort { $a cmp $b } keys %{$index_hash} ) {
337 my $chapter = XML::LibXML::Element->new('chapter');
338 $book->appendChild($chapter);
339 $chapter->setAttribute( 'id', join( '_', 'index', $role ) );
340 $chapter->setAttribute( 'class', 'index' );
341 $chapter->appendTextChild( 'title', ( ucfirst($role) . ' Index' ) );
342 $chapter->appendTextChild( 'title_uri', title_to_uri(ucfirst($role) . ' Index') );
344 foreach my $first ( sort { $a cmp $b } keys %{ $index_hash->{$role} } ) {
345 my $section = XML::LibXML::Element->new('section');
346 my $list = XML::LibXML::Element->new('variablelist');
347 $chapter->appendChild($section);
348 $section->setAttribute( 'id', join( '_', 'index', $role, $first ) );
349 $section->setAttribute( 'class', 'index' );
350 $section->appendTextChild( 'title', $first ? $first : 'Symbols' );
351 $section->appendChild($list);
352 foreach my $primary ( sort { $a cmp $b } keys %{ $index_hash->{$role}{$first} } ) {
353 my $entry = XML::LibXML::Element->new('varlistentry');
354 my $item = XML::LibXML::Element->new('listitem');
355 $list->appendChild($entry)->appendTextChild( 'term', $primary );
356 $entry->appendChild($item);
358 foreach my $secondary ( sort { $a cmp $b } keys %{ $index_hash->{$role}{$first}{$primary} } ) {
359 my $para = XML::LibXML::Element->new('para');
360 if ( $secondary eq '' ) {
361 $item->appendChild($para); # skip having extra layer of heirarchy
365 $slist = XML::LibXML::Element->new('variablelist');
366 $item->appendChild($slist);
368 my $sentry = XML::LibXML::Element->new('varlistentry');
369 my $sitem = XML::LibXML::Element->new('listitem');
370 $slist->appendChild($sentry)->appendTextChild( 'term', $secondary );
371 $sentry->appendChild($sitem)->appendChild($para);
374 foreach my $ref ( @{ $index_hash->{$role}{$first}{$primary}{$secondary} } ) {
375 $para->appendText(', ')
377 my $xrefel = XML::LibXML::Element->new('xref');
378 $xrefel->setAttribute( linkend => $ref );
379 $xrefel->setAttribute( longref => 1 );
380 $para->appendChild($xrefel);
388 # ------------------------------------------------------------------
389 ## Handle the transformation
391 my ( $xml, $xsl_path, $out_path, $staticroot_abs ) = @_;
393 ## make sure $staticroot is set
394 $staticroot_abs ||= $opt{staticroot};
396 ## Build an empty XML structure if an undefined $xml was passed
397 unless ( defined $xml ) {
398 $xml = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
399 $xml->setDocumentElement( $xml->createElement('content') );
402 ## Add the current version of Exim to the XML
403 $xml->documentElement()->appendTextChild( 'current_version', $opt{latest} );
405 ## Add the old versions of Exim to the XML
406 $xml->documentElement()->appendTextChild( 'old_versions', $_ ) foreach old_docs_versions();
408 ## Parse the ".xsl" file as XML
409 my $xsl = XML::LibXML->new()->parse_file($xsl_path) or die $!;
411 ## Generate a stylesheet from the ".xsl" XML.
412 my $stylesheet = XML::LibXSLT->new()->parse_stylesheet($xsl);
414 ## work out the static root relative to the target
415 my $target_dir = ( File::Spec->splitpath($out_path) )[1];
416 my $staticroot = File::Spec->abs2rel( $staticroot_abs, $target_dir );
418 ## Generate a doc from the XML transformed with the XSL
419 my $doc = $stylesheet->transform( $xml, staticroot => sprintf( "'%s'", $staticroot ) );
421 ## Make the containing directory if it doesn't exist
422 make_path( ( $out_path =~ /^(.+)\/.+$/ )[0], { verbose => $opt{verbose} } );
424 ## Write out the document
425 open my $out, '>', $out_path or die "Unable to write $out_path - $!";
426 print $out $stylesheet->output_as_bytes($doc);
430 # ------------------------------------------------------------------
431 ## Takes a chapter title and fixes it up so it is suitable for use in a URI
433 my $title = lc(shift);
434 $title =~ s/[^a-z0-9\s]+//gi; # Only allow spaces, numbers and letters
435 $title =~ s/\s+/_/g; # Replace spaces with underscores so URLs are easier to copy about
439 # ------------------------------------------------------------------
440 ## Look in the docroot for old versions of the documentation
441 sub old_docs_versions {
442 if ( !exists $cache{old_docs_versions} ) {
444 foreach ( glob("$opt{docroot}/exim-html-*") ) {
445 push @versions, $1 if /-(\d+(?:\.\d+)?)$/ && $1 lt $opt{latest} && -d $_;
447 $cache{old_docs_versions} = [ reverse sort { $a cmp $b } @versions ];
449 return @{ $cache{old_docs_versions} };
452 # ------------------------------------------------------------------
458 pod2usage( -exitval => 1, -verbose => 0 );
461 # ------------------------------------------------------------------
463 sub parse_arguments {
465 my %opt = ( spec => [], filter => [], help => 0, man => 0, web => 0, minify => 1, verbose => 0, localstatic => 0 );
467 \%opt, 'help|h!', 'man!', 'web!', 'spec=s{1,}', 'filter=s{1,}',
468 'latest=s', 'tmpl=s', 'docroot=s', 'minify!', 'verbose!', 'localstatic!'
469 ) || pod2usage( -exitval => 1, -verbose => 0 );
472 pod2usage(0) if ( $opt{help} );
473 pod2usage( -verbose => 2 ) if ( $opt{man} );
475 ## --spec and --filter lists
476 foreach my $set (qw[spec filter]) {
478 [ map { my $f = File::Spec->rel2abs($_); error_help( 'No such file: ' . $_ ) unless -f $f; $f }
482 error_help('Missing value for latest') unless ( exists( $opt{latest} ) && defined( $opt{latest} ) );
483 error_help('Invalid value for latest') unless $opt{latest} =~ /^\d+(?:\.\d+)*$/;
485 ## --tmpl and --docroot
486 foreach my $set (qw[tmpl docroot]) {
487 error_help( 'Missing value for ' . $set ) unless ( exists( $opt{$set} ) && defined( $opt{$set} ) );
488 my $f = File::Spec->rel2abs( $opt{$set} );
489 error_help( 'No such directory: ' . $opt{$set} ) unless -d $f;
492 error_help('Excess arguments') if ( scalar(@ARGV) );
494 error_help('Must include at least one of --web, --spec or --filter')
495 unless ( $opt{web} || scalar( @{ $opt{spec} || [] } ) || scalar( @{ $opt{filter} || [] } ) );
500 # ------------------------------------------------------------------
507 gen.pl - Generate exim html documentation and website
514 --help display this help and exits
515 --man displays man page
516 --spec file... spec docbook/XML source files
517 --filter file... filter docbook/XML source files
518 --web Generate the general website pages
519 --latest VERSION Required. Specify the latest stable version of Exim.
520 --tmpl PATH Required. Path to the templates directory
521 --docroot PATH Required. Path to the website document root
522 --[no-]minify [Don't] minify CSS and Javascript
523 --localstatic Makes the static files local to each doc ver
531 Display help and exits
537 =item B<--spec> I<file...>
539 List of files that make up the specification documentation docbook/XML source
542 =item B<--filter> I<file...>
544 List of files that make up the filter documentation docbook/XML source files.
548 Generate the website from the template files.
550 =item B<--latest> I<version>
552 Specify the current exim version. This is used to create links to the current
555 This option is I<required>
557 =item B<--tmpl> I<directory>
559 Specify the directory that the templates are kept in.
561 This option is I<required>
563 =item B<--docroot> I<directory>
565 Specify the directory that the output should be generated into. This is the
566 website C<docroot> directory.
568 This option is I<required>
572 If this option is set then both the CSS and Javascript files processed are
573 minified using L<CSS::Minifier::XS> and L<JavaScript::Minifier::XS>
576 This option is set by default - to disable it specify C<--no-minify>
578 =item B<--localstatic>
580 Makes the static files (CSS, images etc), local for each version of the
581 documentation. This is more suitable for packaged HTML documentation.
587 Generates the exim website and HTML documentation.
593 --spec docbook/*/spec.xml \
594 --filter docbook/*/filter.xml \
597 --docroot /tmp/website
603 Nigel Metheringham <nigel@exim.org> - mostly broke the framework Mike produced.
607 Copyright 2010-2012 Exim Maintainers. All rights reserved.