6 use CSS::Minifier::XS 0.07;
10 use File::Path qw(make_path);
14 use JavaScript::Minifier::XS;
19 my $canonical_url = 'https://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 foreach my $type (qw(html pdf)) {
37 print "Symlinking exim-$type-current to exim-$type-$opt{latest}\n" if ( $opt{verbose} );
38 unlink("$opt{docroot}/exim-$type-current");
39 symlink( "exim-$type-$opt{latest}", "$opt{docroot}/exim-$type-current" )
40 || warn "symlink to $opt{docroot}/exim-$type-current failed";
43 # ------------------------------------------------------------------
44 ## Generate the website files
47 ## copy these templates to docroot...
48 copy_transform_files( "$opt{tmpl}/web", $opt{docroot}, 0 );
51 # ------------------------------------------------------------------
52 ## Generate the static file set
54 my $staticroot = shift || $opt{staticroot};
56 ## make sure I have a directory
57 mkdir($staticroot) or die "Unable to make staticroot: $!\n" unless ( -d $staticroot );
59 ## copy these templates to docroot...
60 copy_transform_files( "$opt{tmpl}/static", $staticroot, 1 );
63 # ------------------------------------------------------------------
64 ## Generate the website files
65 sub copy_transform_files {
70 ## Make sure the template web directory exists
71 die "No such directory: $source\n" unless ( -d $source );
73 ## Scan the web templates
76 my ($path) = substr( $File::Find::name, length("$source"), length($File::Find::name) ) =~ m#^/*(.*)$#;
78 if ( -d "$source/$path" ) {
80 ## Create the directory in the target if it doesn't exist
81 if ( !-d "$target/$path" ) {
82 mkdir("$target/$path") or die "Unable to make $target/$path: $!\n";
88 ## Build HTML from XSL files and simply copy static files which have changed
89 if ( ( !$static ) and ( $path =~ /(.+)\.xsl$/ ) ) {
90 print "Generating : /$1.html\n" if ( $opt{verbose} );
91 transform( undef, "$source/$path", "$target/$1.html" );
93 elsif ( -f "$source/$path" ) {
95 ## Skip if the file hasn't changed (mtime/size based)
97 if (( -f "$target/$path" )
98 and ( ( stat("$source/$path") )[9] == ( stat("$target/$path") )[9] )
99 and ( ( stat("$source/$path") )[7] == ( stat("$target/$path") )[7] ) );
101 if ( $path =~ /(.+)\.css$/ ) {
102 print "CSS to : /$path\n" if ( $opt{verbose} );
103 my $content = read_file("$source/$path");
104 write_file( "$target/$path", $opt{minify} ? CSS::Minifier::XS::minify($content) : $content );
106 elsif ( $path =~ /(.+)\.js$/ ) {
107 print "JS to : /$path\n" if ( $opt{verbose} );
108 my $content = read_file("$source/$path");
109 write_file( "$target/$path",
110 $opt{minify} ? JavaScript::Minifier::XS::minify($content) : $content );
114 print "Copying to : /$path\n" if ( $opt{verbose} );
115 copy( "$source/$path", "$target/$path" ) or die "$path: $!";
118 utime( time, ( stat("$source/$path") )[9], "$target/$path" );
127 # ------------------------------------------------------------------
128 ## Generate index/chapter files for a doc
130 my ( $type, $xml_path ) = @_;
132 ## Read and validate the XML file
133 my $xml = XML::LibXML->new()->parse_file($xml_path) or die $!;
135 ## Get the version number
136 my $version = $xml->findvalue('/book/bookinfo/revhistory/revision/revnumber');
137 die "Unable to get version number\n" unless defined $version && $version =~ /^\d+(\.\d+)*$/;
139 ## Prepend chapter filenames?
140 my $prepend_chapter = $type eq 'filter' ? 'filter_' : '';
142 ## Add the canonical url for this document
143 $xml->documentElement()
144 ->appendTextChild( 'canonical_url',
145 "${canonical_url}exim-html-current/doc/html/spec_html/" . ( $type eq 'spec' ? 'index' : 'filter' ) . ".html" );
147 ## Add a url for the latest version of this document
148 if ( $version ne $opt{latest} ) {
149 $xml->documentElement()
150 ->appendTextChild( 'current_url',
151 "../../../../exim-html-current/doc/html/spec_html/" . ( $type eq 'spec' ? 'index' : 'filter' ) . ".html" );
155 xref_fixup( $xml, $prepend_chapter );
157 ## set the staticroot
160 ? File::Spec->catdir( $opt{docroot}, "exim-html-$version", 'doc', 'html', 'static' )
162 unless ( -d $staticroot ) {
163 make_path( $staticroot, { verbose => $opt{verbose} } );
164 do_static($staticroot);
167 ## Generate the front page
169 my $path = "exim-html-$version/doc/html/spec_html/" . ( $type eq 'filter' ? $type : 'index' ) . ".html";
170 print "Generating : docroot:/$path\n" if ( $opt{verbose} );
171 transform( $xml, "$opt{tmpl}/doc/index.xsl", "$opt{docroot}/$path", $staticroot );
174 ## Generate a Table of Contents XML file
177 "exim-html-$version/doc/html/spec_html/" . ( $type eq 'filter' ? 'filter_toc' : 'index_toc' ) . ".xml";
178 print "Generating : docroot:/$path\n" if ( $opt{verbose} );
179 transform( $xml, "$opt{tmpl}/doc/toc.xsl", "$opt{docroot}/$path", $staticroot );
182 ## Generate the chapters
184 my @chapters = map { $_->cloneNode(1) } $xml->findnodes('/book/chapter');
185 my( $chapter_title, $chapter_title_prev, $chapter_title_next );
186 foreach my $chapter (@chapters) {
188 ## Add a <chapter_id>N</chapter_id> node for the stylesheet to use
189 $chapter->appendTextChild( 'chapter_id', ++$counter );
191 ## Get the current and surrounding chapter titles
192 $chapter_title_prev = $chapter_title;
193 $chapter_title = $chapter_title_next || $chapter->findvalue('title_uri');
194 $chapter_title_next = $chapters[$counter]->findvalue('title_uri') if $counter < int(@chapters);
196 ## Add previous/next/canonical urls for nav
198 $chapter->appendTextChild( 'prev_url',
203 : sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title_prev ) );
204 $chapter->appendTextChild( 'this_url', sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title ) );
205 $chapter->appendTextChild( 'next_url', sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title_next ) )
206 unless int(@chapters) == $counter;
207 $chapter->appendTextChild( 'toc_url', ( $type eq 'filter' ? 'filter' : 'index' ) . '.html' );
208 $chapter->appendTextChild(
211 'https://www.exim.org/exim-html-current/doc/html/spec_html/%sch-%s.html',
212 $prepend_chapter, $chapter_title
215 if ( $version ne $opt{latest} ) {
216 $chapter->appendTextChild(
219 '../../../../exim-html-current/doc/html/spec_html/%sch-%s.html',
220 $prepend_chapter, $chapter_title
226 ## Create an XML document from the chapter
227 my $doc = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
228 $doc->setDocumentElement($chapter);
230 ## Transform the chapter into html
232 my $real_path = sprintf( 'exim-html-%s/doc/html/spec_html/%sch-%s.html', $version, $prepend_chapter, $chapter_title );
233 my $link_path = sprintf( 'exim-html-%s/doc/html/spec_html/%sch%02d.html', $version, $prepend_chapter, $counter );
234 print "Generating : docroot:/$real_path\n" if ( $opt{verbose} );
235 transform( $doc, "$opt{tmpl}/doc/chapter.xsl", "$opt{docroot}/$real_path", $staticroot );
236 # Making a relative symlink to a file in the same directory.
237 # Extract just the filename portion of $real_path.
238 my $real_file = basename($real_path);
239 print "Symlinking : docroot:/$link_path to $real_file\n" if ( $opt{verbose} );
240 if ( -f "$opt{docroot}/$link_path" ) {
241 unlink("$opt{docroot}/$link_path") or die "failed removing $opt{docroot}/$link_path: $!";
243 symlink( "$real_file", "$opt{docroot}/$link_path" ) || die "symlink to $opt{docroot}/$link_path failed: $!";
248 # ------------------------------------------------------------------
251 my ( $xml, $prepend_chapter ) = @_;
255 ## Add the "prepend_chapter" info
256 ( $xml->findnodes('/book') )[0]->appendTextChild( 'prepend_chapter', $prepend_chapter );
258 ## Iterate over each chapter
259 my $chapter_counter = 0;
260 foreach my $chapter ( $xml->findnodes('/book/chapter') ) {
263 my $chapter_id = $chapter->getAttribute('id');
264 unless ($chapter_id) { # synthesise missing id
265 $chapter_id = sprintf( 'chapter_noid_%04d', $chapter_counter );
266 $chapter->setAttribute( 'id', $chapter_id );
268 my $chapter_title = $chapter->findvalue('title');
270 ## Set title_uri so we can use eg ch-introduction.html instead of ch01.html
271 $chapter->appendTextChild( 'title_uri', title_to_uri($chapter_title) );
273 $index{$chapter_id} = { chapter_id => $chapter_counter, chapter_title => $chapter_title };
275 ## Iterate over each section
276 my $section_counter = 0;
277 foreach my $section ( $chapter->findnodes('section') ) {
280 my $section_id = $section->getAttribute('id');
281 unless ($section_id) { # synthesise missing id
282 $section_id = sprintf( 'section_noid_%04d_%04d', $chapter_counter, $section_counter );
283 $section->setAttribute( 'id', $section_id );
285 my $section_title = $section->findvalue('title');
287 $index{$section_id} = {
288 chapter_id => $chapter_counter,
289 chapter_title => $chapter_title,
290 section_id => $section_counter,
291 section_title => $section_title
295 ## Build indexes as new chapters
296 build_indexes( $xml, $prepend_chapter, \%index );
298 ## Replace all of the xrefs in the XML
299 foreach my $xref ( $xml->findnodes('//xref') ) {
300 my $linkend = $xref->getAttribute('linkend');
301 if ( exists $index{$linkend} ) {
302 $xref->setAttribute( 'chapter_id', $index{$linkend}{'chapter_id'} );
303 $xref->setAttribute( 'chapter_title', $index{$linkend}{'chapter_title'} );
304 $xref->setAttribute( 'section_id', $index{$linkend}{'section_id'} ) if ( $index{$linkend}{'section_id'} );
305 $xref->setAttribute( 'section_title', $index{$linkend}{'section_title'} )
306 if ( $index{$linkend}{'section_title'} );
307 $xref->setAttribute( 'url',
308 sprintf( '%sch-%s.html', $prepend_chapter, title_to_uri($index{$linkend}{'chapter_title'}) )
309 . ( $index{$linkend}{'section_id'} ? '#' . $linkend : '' ) );
314 # ------------------------------------------------------------------
317 my ( $xml, $prepend_chapter, $xref ) = @_;
321 foreach my $node ( $xml->findnodes('//section | //chapter | //indexterm') ) {
322 if ( $node->nodeName eq 'indexterm' ) {
323 my $role = $node->getAttribute('role') || 'concept';
324 my $primary = $node->findvalue('child::primary');
325 my $first = ( $primary =~ /^[A-Za-z]/ ) ? uc( substr( $primary, 0, 1 ) ) : ''; # first letter or marker
326 my $secondary = $node->findvalue('child::secondary') || '';
327 next unless ( $primary || $secondary ); # skip blank entries for now...
328 $index_hash->{$role}{$first}{$primary}{$secondary} ||= [];
329 push @{ $index_hash->{$role}{$first}{$primary}{$secondary} }, $current_id;
332 $current_id = $node->getAttribute('id');
336 # now we build a set of new chapters with the index data in
337 my $book = ( $xml->findnodes('/book') )[0];
338 foreach my $role ( sort { $a cmp $b } keys %{$index_hash} ) {
339 my $chapter = XML::LibXML::Element->new('chapter');
340 $book->appendChild($chapter);
341 $chapter->setAttribute( 'id', join( '_', 'index', $role ) );
342 $chapter->setAttribute( 'class', 'index' );
343 $chapter->appendTextChild( 'title', ( ucfirst($role) . ' Index' ) );
344 $chapter->appendTextChild( 'title_uri', title_to_uri(ucfirst($role) . ' Index') );
346 foreach my $first ( sort { $a cmp $b } keys %{ $index_hash->{$role} } ) {
347 my $section = XML::LibXML::Element->new('section');
348 my $list = XML::LibXML::Element->new('variablelist');
349 $chapter->appendChild($section);
350 $section->setAttribute( 'id', join( '_', 'index', $role, $first ) );
351 $section->setAttribute( 'class', 'index' );
352 $section->appendTextChild( 'title', $first ? $first : 'Symbols' );
353 $section->appendChild($list);
354 foreach my $primary ( sort { $a cmp $b } keys %{ $index_hash->{$role}{$first} } ) {
355 my $entry = XML::LibXML::Element->new('varlistentry');
356 my $item = XML::LibXML::Element->new('listitem');
357 $list->appendChild($entry)->appendTextChild( 'term', $primary );
358 $entry->appendChild($item);
360 foreach my $secondary ( sort { $a cmp $b } keys %{ $index_hash->{$role}{$first}{$primary} } ) {
361 my $para = XML::LibXML::Element->new('para');
362 if ( $secondary eq '' ) {
363 $item->appendChild($para); # skip having extra layer of heirarchy
367 $slist = XML::LibXML::Element->new('variablelist');
368 $item->appendChild($slist);
370 my $sentry = XML::LibXML::Element->new('varlistentry');
371 my $sitem = XML::LibXML::Element->new('listitem');
372 $slist->appendChild($sentry)->appendTextChild( 'term', $secondary );
373 $sentry->appendChild($sitem)->appendChild($para);
376 foreach my $ref ( @{ $index_hash->{$role}{$first}{$primary}{$secondary} } ) {
377 $para->appendText(', ')
379 my $xrefel = XML::LibXML::Element->new('xref');
380 $xrefel->setAttribute( linkend => $ref );
381 $xrefel->setAttribute( longref => 1 );
382 $para->appendChild($xrefel);
390 # ------------------------------------------------------------------
391 ## Handle the transformation
393 my ( $xml, $xsl_path, $out_path, $staticroot_abs ) = @_;
395 ## make sure $staticroot is set
396 $staticroot_abs ||= $opt{staticroot};
398 ## Build an empty XML structure if an undefined $xml was passed
399 unless ( defined $xml ) {
400 $xml = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
401 $xml->setDocumentElement( $xml->createElement('content') );
404 ## Add the current version of Exim to the XML
405 $xml->documentElement()->appendTextChild( 'current_version', $opt{latest} );
407 ## Add the old versions of Exim to the XML
408 $xml->documentElement()->appendTextChild( 'old_versions', $_ ) foreach old_docs_versions();
410 ## Parse the ".xsl" file as XML
411 my $xsl = XML::LibXML->new()->parse_file($xsl_path) or die $!;
413 ## Generate a stylesheet from the ".xsl" XML.
414 my $stylesheet = XML::LibXSLT->new()->parse_stylesheet($xsl);
416 ## work out the static root relative to the target
417 my $target_dir = ( File::Spec->splitpath($out_path) )[1];
418 my $staticroot = File::Spec->abs2rel( $staticroot_abs, $target_dir );
420 ## Generate a doc from the XML transformed with the XSL
421 my $doc = $stylesheet->transform( $xml, staticroot => sprintf( "'%s'", $staticroot ) );
423 ## Make the containing directory if it doesn't exist
424 make_path( ( $out_path =~ /^(.+)\/.+$/ )[0], { verbose => $opt{verbose} } );
426 ## Write out the document
427 open my $out, '>', $out_path or die "Unable to write $out_path - $!";
428 print $out $stylesheet->output_as_bytes($doc);
432 # ------------------------------------------------------------------
433 ## Takes a chapter title and fixes it up so it is suitable for use in a URI
435 my $title = lc(shift);
436 $title =~ s/[^a-z0-9\s]+//gi; # Only allow spaces, numbers and letters
437 $title =~ s/\s+/_/g; # Replace spaces with underscores so URLs are easier to copy about
441 # ------------------------------------------------------------------
442 ## Look in the docroot for old versions of the documentation
443 sub old_docs_versions {
444 if ( !exists $cache{old_docs_versions} ) {
446 foreach ( glob("$opt{docroot}/exim-html-*") ) {
447 push @versions, $1 if /-(\d+(?:\.\d+)?)$/ && $1 lt $opt{latest} && -d $_;
449 $cache{old_docs_versions} = [ reverse sort { $a cmp $b } @versions ];
451 return @{ $cache{old_docs_versions} };
454 # ------------------------------------------------------------------
460 pod2usage( -exitval => 1, -verbose => 0 );
463 # ------------------------------------------------------------------
465 sub parse_arguments {
467 my %opt = ( spec => [], filter => [], help => 0, man => 0, web => 0, minify => 1, verbose => 0, localstatic => 0 );
469 \%opt, 'help|h!', 'man!', 'web!', 'spec=s{1,}', 'filter=s{1,}',
470 'latest=s', 'tmpl=s', 'docroot=s', 'minify!', 'verbose!', 'localstatic!'
471 ) || pod2usage( -exitval => 1, -verbose => 0 );
474 pod2usage(0) if ( $opt{help} );
475 pod2usage( -verbose => 2 ) if ( $opt{man} );
477 ## --spec and --filter lists
478 foreach my $set (qw[spec filter]) {
480 [ map { my $f = File::Spec->rel2abs($_); error_help( 'No such file: ' . $_ ) unless -f $f; $f }
484 error_help('Missing value for latest') unless ( exists( $opt{latest} ) && defined( $opt{latest} ) );
485 error_help('Invalid value for latest') unless $opt{latest} =~ /^\d+(?:\.\d+)*$/;
487 ## --tmpl and --docroot
488 foreach my $set (qw[tmpl docroot]) {
489 error_help( 'Missing value for ' . $set ) unless ( exists( $opt{$set} ) && defined( $opt{$set} ) );
490 my $f = File::Spec->rel2abs( $opt{$set} );
491 error_help( 'No such directory: ' . $opt{$set} ) unless -d $f;
494 error_help('Excess arguments') if ( scalar(@ARGV) );
496 error_help('Must include at least one of --web, --spec or --filter')
497 unless ( $opt{web} || scalar( @{ $opt{spec} || [] } ) || scalar( @{ $opt{filter} || [] } ) );
502 # ------------------------------------------------------------------
509 gen.pl - Generate exim html documentation and website
516 --help display this help and exits
517 --man displays man page
518 --spec file... spec docbook/XML source files
519 --filter file... filter docbook/XML source files
520 --web Generate the general website pages
521 --latest VERSION Required. Specify the latest stable version of Exim.
522 --tmpl PATH Required. Path to the templates directory
523 --docroot PATH Required. Path to the website document root
524 --[no-]minify [Don't] minify CSS and Javascript
525 --localstatic Makes the static files local to each doc ver
533 Display help and exits
539 =item B<--spec> I<file...>
541 List of files that make up the specification documentation docbook/XML source
544 =item B<--filter> I<file...>
546 List of files that make up the filter documentation docbook/XML source files.
550 Generate the website from the template files.
552 =item B<--latest> I<version>
554 Specify the current exim version. This is used to create links to the current
557 This option is I<required>
559 =item B<--tmpl> I<directory>
561 Specify the directory that the templates are kept in.
563 This option is I<required>
565 =item B<--docroot> I<directory>
567 Specify the directory that the output should be generated into. This is the
568 website C<docroot> directory.
570 This option is I<required>
574 If this option is set then both the CSS and Javascript files processed are
575 minified using L<CSS::Minifier::XS> and L<JavaScript::Minifier::XS>
578 This option is set by default - to disable it specify C<--no-minify>
580 =item B<--localstatic>
582 Makes the static files (CSS, images etc), local for each version of the
583 documentation. This is more suitable for packaged HTML documentation.
589 Generates the exim website and HTML documentation.
595 --spec docbook/*/spec.xml \
596 --filter docbook/*/filter.xml \
599 --docroot /tmp/website
605 Nigel Metheringham <nigel@exim.org> - mostly broke the framework Mike produced.
609 Copyright 2010-2012 Exim Maintainers. All rights reserved.