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
137 my $version = $xml->findvalue('/book/bookinfo/revhistory/revision/revnumber');
138 die "Unable to get version number\n" unless defined $version && $version =~ /^\d+(\.\d+)*$/;
140 ## Prepend chapter filenames?
141 my $prepend_chapter = $type eq 'filter' ? 'filter_' : '';
143 ## Add the canonical url for this document
144 $xml->documentElement()
145 ->appendTextChild( 'canonical_url',
146 "${canonical_url}exim-html-current/doc/html/spec_html/" . ( $type eq 'spec' ? 'index' : 'filter' ) . ".html" );
148 ## Add a url for the latest version of this document
149 if ( $version ne $opt{latest} ) {
150 $xml->documentElement()
151 ->appendTextChild( 'current_url',
152 "../../../../exim-html-current/doc/html/spec_html/" . ( $type eq 'spec' ? 'index' : 'filter' ) . ".html" );
156 xref_fixup( $xml, $prepend_chapter );
158 ## set the staticroot
161 ? File::Spec->catdir( $opt{docroot}, "exim-html-$version", 'doc', 'html', 'static' )
163 unless ( -d $staticroot ) {
164 make_path( $staticroot, { verbose => $opt{verbose} } );
165 do_static($staticroot);
168 ## Generate the front page
170 my $path = "exim-html-$version/doc/html/spec_html/" . ( $type eq 'filter' ? $type : 'index' ) . ".html";
171 print "Generating : docroot:/$path\n" if ( $opt{verbose} );
172 transform( $xml, "$opt{tmpl}/doc/index.xsl", "$opt{docroot}/$path", $staticroot );
175 ## Generate a Table of Contents XML file
178 "exim-html-$version/doc/html/spec_html/" . ( $type eq 'filter' ? 'filter_toc' : 'index_toc' ) . ".xml";
179 print "Generating : docroot:/$path\n" if ( $opt{verbose} );
180 transform( $xml, "$opt{tmpl}/doc/toc.xsl", "$opt{docroot}/$path", $staticroot );
183 ## Generate the chapters
185 my @chapters = map { $_->cloneNode(1) } $xml->findnodes('/book/chapter');
186 my( $chapter_title, $chapter_title_prev, $chapter_title_next );
187 foreach my $chapter (@chapters) {
189 ## Add a <chapter_id>N</chapter_id> node for the stylesheet to use
190 $chapter->appendTextChild( 'chapter_id', ++$counter );
192 ## Get the current and surrounding chapter titles
193 $chapter_title_prev = $chapter_title;
194 $chapter_title = $chapter_title_next || $chapter->findvalue('title_uri');
195 $chapter_title_next = $chapters[$counter]->findvalue('title_uri') if $counter < int(@chapters);
197 ## Add previous/next/canonical urls for nav
199 $chapter->appendTextChild( 'prev_url',
204 : sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title_prev ) );
205 $chapter->appendTextChild( 'this_url', sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title ) );
206 $chapter->appendTextChild( 'next_url', sprintf( '%sch-%s.html', $prepend_chapter, $chapter_title_next ) )
207 unless int(@chapters) == $counter;
208 $chapter->appendTextChild( 'toc_url', ( $type eq 'filter' ? 'filter' : 'index' ) . '.html' );
209 $chapter->appendTextChild(
212 'https://www.exim.org/exim-html-current/doc/html/spec_html/%sch-%s.html',
213 $prepend_chapter, $chapter_title
216 if ( $version ne $opt{latest} ) {
217 $chapter->appendTextChild(
220 '../../../../exim-html-current/doc/html/spec_html/%sch-%s.html',
221 $prepend_chapter, $chapter_title
227 ## Create an XML document from the chapter
228 my $doc = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
229 $doc->setDocumentElement($chapter);
231 ## Transform the chapter into html
233 my $real_path = sprintf( 'exim-html-%s/doc/html/spec_html/%sch-%s.html', $version, $prepend_chapter, $chapter_title );
234 my $link_path = sprintf( 'exim-html-%s/doc/html/spec_html/%sch%02d.html', $version, $prepend_chapter, $counter );
235 print "Generating : docroot:/$real_path\n" if ( $opt{verbose} );
236 transform( $doc, "$opt{tmpl}/doc/chapter.xsl", "$opt{docroot}/$real_path", $staticroot );
237 # Making a relative symlink to a file in the same directory.
238 # Extract just the filename portion of $real_path.
239 my $real_file = basename($real_path);
240 print "Symlinking : docroot:/$link_path to $real_file\n" if ( $opt{verbose} );
241 if ( -f "$opt{docroot}/$link_path" ) {
242 unlink("$opt{docroot}/$link_path") or die "failed removing $opt{docroot}/$link_path: $!";
244 symlink( "$real_file", "$opt{docroot}/$link_path" ) || die "symlink to $opt{docroot}/$link_path failed: $!";
249 # ------------------------------------------------------------------
252 my ( $xml, $prepend_chapter ) = @_;
256 ## Add the "prepend_chapter" info
257 ( $xml->findnodes('/book') )[0]->appendTextChild( 'prepend_chapter', $prepend_chapter );
259 ## Iterate over each chapter
260 my $chapter_counter = 0;
261 foreach my $chapter ( $xml->findnodes('/book/chapter') ) {
264 my $chapter_id = $chapter->getAttribute('id');
265 unless ($chapter_id) { # synthesise missing id
266 $chapter_id = sprintf( 'chapter_noid_%04d', $chapter_counter );
267 $chapter->setAttribute( 'id', $chapter_id );
269 my $chapter_title = $chapter->findvalue('title');
271 ## Set title_uri so we can use eg ch-introduction.html instead of ch01.html
272 $chapter->appendTextChild( 'title_uri', title_to_uri($chapter_title) );
274 $index{$chapter_id} = { chapter_id => $chapter_counter, chapter_title => $chapter_title };
276 ## Iterate over each section
277 my $section_counter = 0;
278 foreach my $section ( $chapter->findnodes('section') ) {
281 my $section_id = $section->getAttribute('id');
282 unless ($section_id) { # synthesise missing id
283 $section_id = sprintf( 'section_noid_%04d_%04d', $chapter_counter, $section_counter );
284 $section->setAttribute( 'id', $section_id );
286 my $section_title = $section->findvalue('title');
288 $index{$section_id} = {
289 chapter_id => $chapter_counter,
290 chapter_title => $chapter_title,
291 section_id => $section_counter,
292 section_title => $section_title
296 ## Build indexes as new chapters
297 build_indexes( $xml, $prepend_chapter, \%index );
299 ## Replace all of the xrefs in the XML
300 foreach my $xref ( $xml->findnodes('//xref') ) {
301 my $linkend = $xref->getAttribute('linkend');
302 if ( exists $index{$linkend} ) {
303 $xref->setAttribute( 'chapter_id', $index{$linkend}{'chapter_id'} );
304 $xref->setAttribute( 'chapter_title', $index{$linkend}{'chapter_title'} );
305 $xref->setAttribute( 'section_id', $index{$linkend}{'section_id'} ) if ( $index{$linkend}{'section_id'} );
306 $xref->setAttribute( 'section_title', $index{$linkend}{'section_title'} )
307 if ( $index{$linkend}{'section_title'} );
308 $xref->setAttribute( 'url',
309 sprintf( '%sch-%s.html', $prepend_chapter, title_to_uri($index{$linkend}{'chapter_title'}) )
310 . ( $index{$linkend}{'section_id'} ? '#' . $linkend : '' ) );
315 # ------------------------------------------------------------------
318 my ( $xml, $prepend_chapter, $xref ) = @_;
322 foreach my $node ( $xml->findnodes('//section | //chapter | //indexterm') ) {
323 if ( $node->nodeName eq 'indexterm' ) {
324 my $role = $node->getAttribute('role') || 'concept';
325 my $primary = $node->findvalue('child::primary');
326 my $first = ( $primary =~ /^[A-Za-z]/ ) ? uc( substr( $primary, 0, 1 ) ) : ''; # first letter or marker
327 my $secondary = $node->findvalue('child::secondary') || '';
328 next unless ( $primary || $secondary ); # skip blank entries for now...
329 $index_hash->{$role}{$first}{$primary}{$secondary} ||= [];
330 push @{ $index_hash->{$role}{$first}{$primary}{$secondary} }, $current_id;
333 $current_id = $node->getAttribute('id');
337 # now we build a set of new chapters with the index data in
338 my $book = ( $xml->findnodes('/book') )[0];
339 foreach my $role ( sort { $a cmp $b } keys %{$index_hash} ) {
340 my $chapter = XML::LibXML::Element->new('chapter');
341 $book->appendChild($chapter);
342 $chapter->setAttribute( 'id', join( '_', 'index', $role ) );
343 $chapter->setAttribute( 'class', 'index' );
344 $chapter->appendTextChild( 'title', ( ucfirst($role) . ' Index' ) );
345 $chapter->appendTextChild( 'title_uri', title_to_uri(ucfirst($role) . ' Index') );
347 foreach my $first ( sort { $a cmp $b } keys %{ $index_hash->{$role} } ) {
348 my $section = XML::LibXML::Element->new('section');
349 my $list = XML::LibXML::Element->new('variablelist');
350 $chapter->appendChild($section);
351 $section->setAttribute( 'id', join( '_', 'index', $role, $first ) );
352 $section->setAttribute( 'class', 'index' );
353 $section->appendTextChild( 'title', $first ? $first : 'Symbols' );
354 $section->appendChild($list);
355 foreach my $primary ( sort { $a cmp $b } keys %{ $index_hash->{$role}{$first} } ) {
356 my $entry = XML::LibXML::Element->new('varlistentry');
357 my $item = XML::LibXML::Element->new('listitem');
358 $list->appendChild($entry)->appendTextChild( 'term', $primary );
359 $entry->appendChild($item);
361 foreach my $secondary ( sort { $a cmp $b } keys %{ $index_hash->{$role}{$first}{$primary} } ) {
362 my $para = XML::LibXML::Element->new('para');
363 if ( $secondary eq '' ) {
364 $item->appendChild($para); # skip having extra layer of heirarchy
368 $slist = XML::LibXML::Element->new('variablelist');
369 $item->appendChild($slist);
371 my $sentry = XML::LibXML::Element->new('varlistentry');
372 my $sitem = XML::LibXML::Element->new('listitem');
373 $slist->appendChild($sentry)->appendTextChild( 'term', $secondary );
374 $sentry->appendChild($sitem)->appendChild($para);
377 foreach my $ref ( @{ $index_hash->{$role}{$first}{$primary}{$secondary} } ) {
378 $para->appendText(', ')
380 my $xrefel = XML::LibXML::Element->new('xref');
381 $xrefel->setAttribute( linkend => $ref );
382 $xrefel->setAttribute( longref => 1 );
383 $para->appendChild($xrefel);
391 # ------------------------------------------------------------------
392 ## Handle the transformation
394 my ( $xml, $xsl_path, $out_path, $staticroot_abs ) = @_;
396 ## make sure $staticroot is set
397 $staticroot_abs ||= $opt{staticroot};
399 ## Build an empty XML structure if an undefined $xml was passed
400 unless ( defined $xml ) {
401 $xml = XML::LibXML::Document->createDocument( '1.0', 'UTF-8' );
402 $xml->setDocumentElement( $xml->createElement('content') );
405 ## Add the current version of Exim to the XML
406 $xml->documentElement()->appendTextChild( 'current_version', $opt{latest} );
408 ## Add the old versions of Exim to the XML
409 $xml->documentElement()->appendTextChild( 'old_versions', $_ ) foreach old_docs_versions();
411 ## Parse the ".xsl" file as XML
412 my $xsl = XML::LibXML->new()->parse_file($xsl_path) or die $!;
414 ## Generate a stylesheet from the ".xsl" XML.
415 my $stylesheet = XML::LibXSLT->new()->parse_stylesheet($xsl);
417 ## work out the static root relative to the target
418 my $target_dir = ( File::Spec->splitpath($out_path) )[1];
419 my $staticroot = File::Spec->abs2rel( $staticroot_abs, $target_dir );
421 ## Generate a doc from the XML transformed with the XSL
422 my $doc = $stylesheet->transform( $xml, staticroot => sprintf( "'%s'", $staticroot ) );
424 ## Make the containing directory if it doesn't exist
425 make_path( ( $out_path =~ /^(.+)\/.+$/ )[0], { verbose => $opt{verbose} } );
427 ## Write out the document
428 open my $out, '>', $out_path or die "Unable to write $out_path - $!";
429 print $out $stylesheet->output_as_bytes($doc);
433 # ------------------------------------------------------------------
434 ## Takes a chapter title and fixes it up so it is suitable for use in a URI
436 my $title = lc(shift);
437 $title =~ s/[^a-z0-9\s]+//gi; # Only allow spaces, numbers and letters
438 $title =~ s/\s+/_/g; # Replace spaces with underscores so URLs are easier to copy about
442 # ------------------------------------------------------------------
443 ## Look in the docroot for old versions of the documentation
444 sub old_docs_versions {
445 if ( !exists $cache{old_docs_versions} ) {
447 foreach ( glob("$opt{docroot}/exim-html-*") ) {
448 push @versions, $1 if /-(\d+(?:\.\d+)?)$/ && $1 lt $opt{latest} && -d $_;
450 $cache{old_docs_versions} = [ reverse sort { $a cmp $b } @versions ];
452 return @{ $cache{old_docs_versions} };
455 # ------------------------------------------------------------------
461 pod2usage( -exitval => 1, -verbose => 0 );
464 # ------------------------------------------------------------------
466 sub parse_arguments {
468 my %opt = ( spec => [], filter => [], help => 0, man => 0, web => 0, minify => 1, verbose => 0, localstatic => 0, tmpl => "$Bin/../templates" );
470 \%opt, 'help|h!', 'man!', 'web!', 'spec=s{1,}', 'filter=s{1,}',
471 'latest=s', 'tmpl=s', 'docroot=s', 'minify!', 'verbose!', 'localstatic!'
472 ) || pod2usage( -exitval => 1, -verbose => 0 );
475 pod2usage(0) if ( $opt{help} );
476 pod2usage( -verbose => 2 ) if ( $opt{man} );
478 ## --spec and --filter lists
479 foreach my $set (qw[spec filter]) {
481 [ map { my $f = File::Spec->rel2abs($_); error_help( 'No such file: ' . $_ ) unless -f $f; $f }
485 error_help('Missing value for latest') unless ( exists( $opt{latest} ) && defined( $opt{latest} ) );
486 error_help('Invalid value for latest') unless $opt{latest} =~ /^\d+(?:\.\d+)*$/;
488 ## --tmpl and --docroot
489 foreach my $set (qw[tmpl docroot]) {
490 error_help( 'Missing value for ' . $set ) unless ( exists( $opt{$set} ) && defined( $opt{$set} ) );
491 my $f = File::Spec->rel2abs( $opt{$set} );
492 error_help( 'No such directory: ' . $opt{$set} ) unless -d $f;
495 error_help('Excess arguments') if ( scalar(@ARGV) );
497 error_help('Must include at least one of --web, --spec or --filter')
498 unless ( $opt{web} || scalar( @{ $opt{spec} || [] } ) || scalar( @{ $opt{filter} || [] } ) );
503 # ------------------------------------------------------------------
510 gen.pl - Generate exim html documentation and website
517 --help display this help and exits
518 --man displays man page
519 --spec file... spec docbook/XML source files
520 --filter file... filter docbook/XML source files
521 --web Generate the general website pages
522 --latest VERSION Required. Specify the latest stable version of Exim.
523 --tmpl PATH Required. Path to the templates directory
524 --docroot PATH Required. Path to the website document root
525 --[no-]minify [Don't] minify CSS and Javascript
526 --localstatic Makes the static files local to each doc ver
534 Display help and exits
540 =item B<--spec> I<file...>
542 List of files that make up the specification documentation docbook/XML source
545 =item B<--filter> I<file...>
547 List of files that make up the filter documentation docbook/XML source files.
551 Generate the website from the template files.
553 =item B<--latest> I<version>
555 Specify the current exim version. This is used to create links to the current
558 This option is I<required>
560 =item B<--tmpl> I<directory>
562 Specify the directory that the templates are kept in.
564 This option is I<required>
566 =item B<--docroot> I<directory>
568 Specify the directory that the output should be generated into. This is the
569 website C<docroot> directory.
571 This option is I<required>
575 If this option is set then both the CSS and Javascript files processed are
576 minified using L<CSS::Minifier::XS> and L<JavaScript::Minifier::XS>
579 This option is set by default - to disable it specify C<--no-minify>
581 =item B<--localstatic>
583 Makes the static files (CSS, images etc), local for each version of the
584 documentation. This is more suitable for packaged HTML documentation.
590 Generates the exim website and HTML documentation.
596 --spec docbook/*/spec.xml \
597 --filter docbook/*/filter.xml \
600 --docroot /tmp/website
606 Nigel Metheringham <nigel@exim.org> - mostly broke the framework Mike produced.
610 Copyright 2010-2012 Exim Maintainers. All rights reserved.