6 use CSS::Minifier::XS 0.07;
10 use File::Path qw(make_path);
14 use JavaScript::Minifier::XS;
20 my $canonical_url = 'https://www.exim.org/';
23 my %opt = parse_arguments();
25 ## setup static root location
26 ## TODO: for doc generation only this should be within the docs dir
27 $opt{staticroot} = File::Spec->catdir( $opt{docroot}, 'static' );
30 my %cache; # General cache object
31 do_doc( 'spec', $_ ) foreach @{ $opt{spec} || [] };
32 do_doc( 'filter', $_ ) foreach @{ $opt{filter} || [] };
33 do_web() if ( $opt{web} );
34 do_static() if ( $opt{web} or !$opt{localstatic} ); # need this for all other pages generated
36 ## Add the exim-html-current symlink
37 foreach my $type (qw(html pdf)) {
38 print "Symlinking exim-$type-current to exim-$type-$opt{latest}\n" if ( $opt{verbose} );
39 unlink("$opt{docroot}/exim-$type-current");
40 symlink( "exim-$type-$opt{latest}", "$opt{docroot}/exim-$type-current" )
41 || warn "symlink to $opt{docroot}/exim-$type-current failed";
44 # ------------------------------------------------------------------
45 ## Generate the website files
48 ## copy these templates to docroot...
49 copy_transform_files( "$opt{tmpl}/web", $opt{docroot}, 0 );
52 # ------------------------------------------------------------------
53 ## Generate the static file set
55 my $staticroot = shift || $opt{staticroot};
57 ## make sure I have a directory
58 mkdir($staticroot) or die "Unable to make staticroot: $!\n" unless ( -d $staticroot );
60 ## copy these templates to docroot...
61 copy_transform_files( "$opt{tmpl}/static", $staticroot, 1 );
64 # ------------------------------------------------------------------
65 ## Generate the website files
66 sub copy_transform_files {
71 ## Make sure the template web directory exists
72 die "No such directory: $source\n" unless ( -d $source );
74 ## Scan the web templates
77 my ($path) = substr( $File::Find::name, length("$source"), length($File::Find::name) ) =~ m#^/*(.*)$#;
79 if ( -d "$source/$path" ) {
81 ## Create the directory in the target if it doesn't exist
82 if ( !-d "$target/$path" ) {
83 mkdir("$target/$path") or die "Unable to make $target/$path: $!\n";
89 ## Build HTML from XSL files and simply copy static files which have changed
90 if ( ( !$static ) and ( $path =~ /(.+)\.xsl$/ ) ) {
91 print "Generating : /$1.html\n" if ( $opt{verbose} );
92 transform( undef, "$source/$path", "$target/$1.html" );
94 elsif ( -f "$source/$path" ) {
96 ## Skip if the file hasn't changed (mtime/size based)
98 if (( -f "$target/$path" )
99 and ( ( stat("$source/$path") )[9] == ( stat("$target/$path") )[9] )
100 and ( ( stat("$source/$path") )[7] == ( stat("$target/$path") )[7] ) );
102 if ( $path =~ /(.+)\.css$/ ) {
103 print "CSS to : /$path\n" if ( $opt{verbose} );
104 my $content = read_file("$source/$path");
105 write_file( "$target/$path", $opt{minify} ? CSS::Minifier::XS::minify($content) : $content );
107 elsif ( $path =~ /(.+)\.js$/ ) {
108 print "JS to : /$path\n" if ( $opt{verbose} );
109 my $content = read_file("$source/$path");
110 write_file( "$target/$path",
111 $opt{minify} ? JavaScript::Minifier::XS::minify($content) : $content );
115 print "Copying to : /$path\n" if ( $opt{verbose} );
116 copy( "$source/$path", "$target/$path" ) or die "$path: $!";
119 utime( time, ( stat("$source/$path") )[9], "$target/$path" );
128 # ------------------------------------------------------------------
129 ## Generate index/chapter files for a doc
131 my ( $type, $xml_path ) = @_;
133 ## Read and validate the XML file
134 my $xml = XML::LibXML->new()->parse_file($xml_path) or die $!;
136 ## Get the version number
138 my $version = $xml->findvalue('/book/bookinfo/revhistory/revision/revnumber');
139 die "Unable to get version number\n"
140 unless defined $version and $version =~ /^
143 (?:\.\d+(?:\.\d+)?)? # (minor(.patch))
145 (?:-RC\d+)?$/x; # -RCX
149 ## Prepend chapter filenames?
150 my $prepend_chapter = $type eq 'filter' ? 'filter_' : '';
152 ## Add the canonical url for this document
153 $xml->documentElement()
154 ->appendTextChild( 'canonical_url',
155 "${canonical_url}exim-html-current/doc/html/spec_html/" . ( $type eq 'spec' ? 'index' : 'filter' ) . ".html" );
157 ## Add a url for the latest version of this document
158 if ( $version ne $opt{latest} ) {
159 $xml->documentElement()
160 ->appendTextChild( 'current_url',
161 "../../../../exim-html-current/doc/html/spec_html/" . ( $type eq 'spec' ? 'index' : 'filter' ) . ".html" );
165 xref_fixup( $xml, $prepend_chapter );
167 ## set the staticroot
170 ? File::Spec->catdir( $opt{docroot}, "exim-html-$version", 'doc', 'html', 'static' )
172 unless ( -d $staticroot ) {
173 make_path( $staticroot, { verbose => $opt{verbose} } );
174 do_static($staticroot);
177 ## Generate the front page
179 my $path = "exim-html-$version/doc/html/spec_html/" . ( $type eq 'filter' ? $type : 'index' ) . ".html";
180 print "Generating : docroot:/$path\n" if ( $opt{verbose} );
181 transform( $xml, "$opt{tmpl}/doc/index.xsl", "$opt{docroot}/$path", $staticroot );
184 ## Generate a Table of Contents XML file
187 "exim-html-$version/doc/html/spec_html/" . ( $type eq 'filter' ? 'filter_toc' : 'index_toc' ) . ".xml";
188 print "Generating : docroot:/$path\n" if ( $opt{verbose} );
189 transform( $xml, "$opt{tmpl}/doc/toc.xsl", "$opt{docroot}/$path", $staticroot );
192 ## Generate the chapters
194 my @chapters = map { $_->cloneNode(1) } $xml->findnodes('/book/chapter');
195 my( $chapter_title, $chapter_title_prev, $chapter_title_next );
196 foreach my $chapter (@chapters) {
198 ## Add a <chapter_id>N</chapter_id> node for the stylesheet to use
199 $chapter->appendTextChild( 'chapter_id', ++$counter );
201 ## Get the current and surrounding chapter titles
202 $chapter_title_prev = $chapter_title;
203 $chapter_title = $chapter_title_next || $chapter->findvalue('title_uri');
204 $chapter_title_next = $chapters[$counter]->findvalue('title_uri') if $counter < int(@chapters);
206 ## Add previous/next/canonical urls for nav
208 $chapter->appendTextChild( 'prev_url',
213 : sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title_prev ) );
214 $chapter->appendTextChild( 'this_url', sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title ) );
215 $chapter->appendTextChild( 'next_url', sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title_next ) )
216 unless int(@chapters) == $counter;
217 $chapter->appendTextChild( 'toc_url', ( $type eq 'filter' ? 'filter' : 'index' ) . '.html' );
218 $chapter->appendTextChild(
221 'https://www.exim.org/exim-html-current/doc/html/spec_html/%sch-%s.html',
222 $prepend_chapter, $chapter_title
225 if ( $version ne $opt{latest} ) {
226 $chapter->appendTextChild(
229 '../../../../exim-html-current/doc/html/spec_html/%sch-%s.html',
230 $prepend_chapter, $chapter_title
236 ## Create an XML document from the chapter
237 my $doc = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
238 $doc->setDocumentElement($chapter);
240 ## Transform the chapter into html
242 my $real_path = sprintf( 'exim-html-%s/doc/html/spec_html/%sch-%s.html', $version, $prepend_chapter, $chapter_title );
243 my $link_path = sprintf( 'exim-html-%s/doc/html/spec_html/%sch%02d.html', $version, $prepend_chapter, $counter );
244 print "Generating : docroot:/$real_path\n" if ( $opt{verbose} );
245 transform( $doc, "$opt{tmpl}/doc/chapter.xsl", "$opt{docroot}/$real_path", $staticroot );
246 # Making a relative symlink to a file in the same directory.
247 # Extract just the filename portion of $real_path.
248 my $real_file = basename($real_path);
249 print "Symlinking : docroot:/$link_path to $real_file\n" if ( $opt{verbose} );
250 if ( -f "$opt{docroot}/$link_path" ) {
251 unlink("$opt{docroot}/$link_path") or die "failed removing $opt{docroot}/$link_path: $!";
253 symlink( "$real_file", "$opt{docroot}/$link_path" ) || die "symlink to $opt{docroot}/$link_path failed: $!";
258 # ------------------------------------------------------------------
261 my ( $xml, $prepend_chapter ) = @_;
265 ## Add the "prepend_chapter" info
266 ( $xml->findnodes('/book') )[0]->appendTextChild( 'prepend_chapter', $prepend_chapter );
268 ## Iterate over each chapter
269 my $chapter_counter = 0;
270 foreach my $chapter ( $xml->findnodes('/book/chapter') ) {
273 my $chapter_id = $chapter->getAttribute('id');
274 unless ($chapter_id) { # synthesise missing id
275 $chapter_id = sprintf( 'chapter_noid_%04d', $chapter_counter );
276 $chapter->setAttribute( 'id', $chapter_id );
278 my $chapter_title = $chapter->findvalue('title');
280 ## Set title_uri so we can use eg ch-introduction.html instead of ch01.html
281 $chapter->appendTextChild( 'title_uri', title_to_uri($chapter_title) );
283 $index{$chapter_id} = { chapter_id => $chapter_counter, chapter_title => $chapter_title };
285 ## Iterate over each section
286 my $section_counter = 0;
287 foreach my $section ( $chapter->findnodes('section') ) {
290 my $section_id = $section->getAttribute('id');
291 unless ($section_id) { # synthesise missing id
292 $section_id = sprintf( 'section_noid_%04d_%04d', $chapter_counter, $section_counter );
293 $section->setAttribute( 'id', $section_id );
295 my $section_title = $section->findvalue('title');
297 $index{$section_id} = {
298 chapter_id => $chapter_counter,
299 chapter_title => $chapter_title,
300 section_id => $section_counter,
301 section_title => $section_title
305 ## Build indexes as new chapters
306 build_indexes( $xml, $prepend_chapter, \%index );
308 ## Replace all of the xrefs in the XML
309 foreach my $xref ( $xml->findnodes('//xref') ) {
310 my $linkend = $xref->getAttribute('linkend');
311 if ( exists $index{$linkend} ) {
312 $xref->setAttribute( 'chapter_id', $index{$linkend}{'chapter_id'} );
313 $xref->setAttribute( 'chapter_title', $index{$linkend}{'chapter_title'} );
314 $xref->setAttribute( 'section_id', $index{$linkend}{'section_id'} ) if ( $index{$linkend}{'section_id'} );
315 $xref->setAttribute( 'section_title', $index{$linkend}{'section_title'} )
316 if ( $index{$linkend}{'section_title'} );
317 $xref->setAttribute( 'url',
318 sprintf( '%sch-%s.html', $prepend_chapter, title_to_uri($index{$linkend}{'chapter_title'}) )
319 . ( $index{$linkend}{'section_id'} ? '#' . $linkend : '' ) );
324 # ------------------------------------------------------------------
327 my ( $xml, $prepend_chapter, $xref ) = @_;
331 foreach my $node ( $xml->findnodes('//section | //chapter | //indexterm') ) {
332 if ( $node->nodeName eq 'indexterm' ) {
333 my $role = $node->getAttribute('role') || 'concept';
334 my $primary = $node->findvalue('child::primary');
335 my $first = ( $primary =~ /^[A-Za-z]/ ) ? uc( substr( $primary, 0, 1 ) ) : ''; # first letter or marker
336 my $secondary = $node->findvalue('child::secondary') || '';
337 next unless ( $primary || $secondary ); # skip blank entries for now...
338 $index_hash->{$role}{$first}{$primary}{$secondary} ||= [];
339 push @{ $index_hash->{$role}{$first}{$primary}{$secondary} }, $current_id;
342 $current_id = $node->getAttribute('id');
346 # now we build a set of new chapters with the index data in
347 my $book = ( $xml->findnodes('/book') )[0];
348 foreach my $role ( sort { $a cmp $b } keys %{$index_hash} ) {
349 my $chapter = XML::LibXML::Element->new('chapter');
350 $book->appendChild($chapter);
351 $chapter->setAttribute( 'id', join( '_', 'index', $role ) );
352 $chapter->setAttribute( 'class', 'index' );
353 $chapter->appendTextChild( 'title', ( ucfirst($role) . ' Index' ) );
354 $chapter->appendTextChild( 'title_uri', title_to_uri(ucfirst($role) . ' Index') );
356 foreach my $first ( sort { $a cmp $b } keys %{ $index_hash->{$role} } ) {
357 my $section = XML::LibXML::Element->new('section');
358 my $list = XML::LibXML::Element->new('variablelist');
359 $chapter->appendChild($section);
360 $section->setAttribute( 'id', join( '_', 'index', $role, $first ) );
361 $section->setAttribute( 'class', 'index' );
362 $section->appendTextChild( 'title', $first ? $first : 'Symbols' );
363 $section->appendChild($list);
364 foreach my $primary ( sort { $a cmp $b } keys %{ $index_hash->{$role}{$first} } ) {
365 my $entry = XML::LibXML::Element->new('varlistentry');
366 my $item = XML::LibXML::Element->new('listitem');
367 $list->appendChild($entry)->appendTextChild( 'term', $primary );
368 $entry->appendChild($item);
370 foreach my $secondary ( sort { $a cmp $b } keys %{ $index_hash->{$role}{$first}{$primary} } ) {
371 my $para = XML::LibXML::Element->new('para');
372 if ( $secondary eq '' ) {
373 $item->appendChild($para); # skip having extra layer of heirarchy
377 $slist = XML::LibXML::Element->new('variablelist');
378 $item->appendChild($slist);
380 my $sentry = XML::LibXML::Element->new('varlistentry');
381 my $sitem = XML::LibXML::Element->new('listitem');
382 $slist->appendChild($sentry)->appendTextChild( 'term', $secondary );
383 $sentry->appendChild($sitem)->appendChild($para);
386 foreach my $ref ( @{ $index_hash->{$role}{$first}{$primary}{$secondary} } ) {
387 $para->appendText(', ')
389 my $xrefel = XML::LibXML::Element->new('xref');
390 $xrefel->setAttribute( linkend => $ref );
391 $xrefel->setAttribute( longref => 1 );
392 $para->appendChild($xrefel);
400 # ------------------------------------------------------------------
401 ## Handle the transformation
403 my ( $xml, $xsl_path, $out_path, $staticroot_abs ) = @_;
405 ## make sure $staticroot is set
406 $staticroot_abs ||= $opt{staticroot};
408 ## Build an empty XML structure if an undefined $xml was passed
409 unless ( defined $xml ) {
410 $xml = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
411 $xml->setDocumentElement( $xml->createElement('content') );
414 ## Add the current version of Exim to the XML
415 $xml->documentElement()->appendTextChild( 'current_version', $opt{latest} );
417 ## Add the old versions of Exim to the XML
418 $xml->documentElement()->appendTextChild( 'old_versions', $_ ) foreach old_docs_versions();
420 ## Parse the ".xsl" file as XML
421 my $xsl = XML::LibXML->new()->parse_file($xsl_path) or die $!;
423 ## Generate a stylesheet from the ".xsl" XML.
424 my $stylesheet = XML::LibXSLT->new()->parse_stylesheet($xsl);
426 ## work out the static root relative to the target
427 my $target_dir = ( File::Spec->splitpath($out_path) )[1];
428 my $staticroot = File::Spec->abs2rel( $staticroot_abs, $target_dir );
430 ## Generate a doc from the XML transformed with the XSL
431 my $doc = $stylesheet->transform( $xml, staticroot => sprintf( "'%s'", $staticroot ) );
433 ## Make the containing directory if it doesn't exist
434 make_path( ( $out_path =~ /^(.+)\/.+$/ )[0], { verbose => $opt{verbose} } );
436 ## Write out the document
437 open my $out, '>', $out_path or die "Unable to write $out_path - $!";
438 print $out $stylesheet->output_as_bytes($doc);
442 # ------------------------------------------------------------------
443 ## Takes a chapter title and fixes it up so it is suitable for use in a URI
445 my $title = lc(shift);
446 $title =~ s/[^a-z0-9\s]+//gi; # Only allow spaces, numbers and letters
447 $title =~ s/\s+/_/g; # Replace spaces with underscores so URLs are easier to copy about
451 # ------------------------------------------------------------------
452 ## Look in the docroot for old versions of the documentation
453 sub old_docs_versions {
454 if ( !exists $cache{old_docs_versions} ) {
456 foreach ( glob("$opt{docroot}/exim-html-*") ) {
457 push @versions, $1 if /-(\d+(?:\.\d+)?)$/ && $1 lt $opt{latest} && -d $_;
459 $cache{old_docs_versions} = [ reverse sort { $a cmp $b } @versions ];
461 return @{ $cache{old_docs_versions} };
464 # ------------------------------------------------------------------
470 pod2usage( -exitval => 1, -verbose => 0 );
473 # ------------------------------------------------------------------
475 sub parse_arguments {
477 my %opt = ( spec => [], filter => [], help => 0, man => 0, web => 0, minify => 1, verbose => 0, localstatic => 0, tmpl => "$Bin/../templates" );
479 \%opt, 'help|h!', 'man!', 'web!', 'spec=s{1,}', 'filter=s{1,}',
480 'latest=s', 'tmpl=s', 'docroot=s', 'minify!', 'verbose!', 'localstatic!'
481 ) || pod2usage( -exitval => 1, -verbose => 0 );
484 pod2usage(0) if ( $opt{help} );
485 pod2usage( -verbose => 2 ) if ( $opt{man} );
487 ## --spec and --filter lists
488 foreach my $set (qw[spec filter]) {
490 [ map { my $f = File::Spec->rel2abs($_); error_help( 'No such file: ' . $_ ) unless -f $f; $f }
494 error_help('Missing value for latest') unless ( exists( $opt{latest} ) && defined( $opt{latest} ) );
495 error_help('Invalid value for latest') unless $opt{latest} =~ /^\d+(?:\.\d+)*$/;
497 ## --tmpl and --docroot
498 foreach my $set (qw[tmpl docroot]) {
499 error_help( 'Missing value for ' . $set ) unless ( exists( $opt{$set} ) && defined( $opt{$set} ) );
500 my $f = File::Spec->rel2abs( $opt{$set} );
501 error_help( 'No such directory: ' . $opt{$set} ) unless -d $f;
504 error_help('Excess arguments') if ( scalar(@ARGV) );
506 error_help('Must include at least one of --web, --spec or --filter')
507 unless ( $opt{web} || scalar( @{ $opt{spec} || [] } ) || scalar( @{ $opt{filter} || [] } ) );
512 # ------------------------------------------------------------------
519 gen - Generate exim html documentation and website
526 --help display this help and exits
527 --man displays man page
528 --spec file... spec docbook/XML source files
529 --filter file... filter docbook/XML source files
530 --web Generate the general website pages
531 --latest VERSION Required. Specify the latest stable version of Exim.
532 --tmpl PATH Required. Path to the templates directory
533 --docroot PATH Required. Path to the website document root
534 --[no-]minify [Don't] minify CSS and Javascript
535 --localstatic Makes the static files local to each doc ver
543 Display help and exits
549 =item B<--spec> I<file...>
551 List of files that make up the specification documentation docbook/XML source
554 =item B<--filter> I<file...>
556 List of files that make up the filter documentation docbook/XML source files.
560 Generate the website from the template files.
562 =item B<--latest> I<version>
564 Specify the current exim version. This is used to create links to the current
567 This option is I<required>
569 =item B<--tmpl> I<directory>
571 Specify the directory that the templates are kept in.
573 This option is I<required>
575 =item B<--docroot> I<directory>
577 Specify the directory that the output should be generated into. This is the
578 website C<docroot> directory.
580 This option is I<required>
584 If this option is set then both the CSS and Javascript files processed are
585 minified using L<CSS::Minifier::XS> and L<JavaScript::Minifier::XS>
588 This option is set by default - to disable it specify C<--no-minify>
590 =item B<--localstatic>
592 Makes the static files (CSS, images etc), local for each version of the
593 documentation. This is more suitable for packaged HTML documentation.
599 Generates the exim website and HTML documentation.
605 --spec docbook/*/spec.xml \
606 --filter docbook/*/filter.xml \
609 --docroot /tmp/website
615 Nigel Metheringham <nigel@exim.org> - mostly broke the framework Mike produced.
619 Copyright 2010-2012 Exim Maintainers. All rights reserved.