#! /usr/bin/perl

# Copyright © 2004-2008 Brendt Wohlberg <software@wohlberg.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License at
# http://www.gnu.org/licenses/gpl-2.0.txt.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.

# Most recent modification: 18 January 2008

use strict;
use Getopt::Long qw(:config no_ignore_case);
use File::Basename;

#########################################################################

# Location of xmllint and xsltproc binaries
my $xmllint = 'xmllint';
my $xsltproc = 'xsltproc';

# Location of include files
my $sharepath = $ENV{'PWD'}.'/'.dirname($0);
my $incpath = "$sharepath/include";
my $xmlpath = "$sharepath/xml";

# XNav version
my $xnversion = `cat $sharepath/version`; chomp($xnversion);


# Handle command line parsing
my $vrsnflag=0;
my $vrbsflag=0;
my $helpflag=0;
my $hdeltlst='all';
my $stylebdy=undef;
my $didxflag=0;
my $usagetext="usage: xnav [-V] [-h] [-v] [-c (all | none | ".
              "(title|base|script|style|meta|link|object) [,".
              "(title|base|script|style|meta|link|object)][,...])] ".
              "[-b (rmv | div)] [-i] [ init | make | valid | clean ] path\n";
GetOptions("V+" =>  \$vrsnflag,
           "h+" =>  \$helpflag,
           "v+" =>  \$vrbsflag,
           "c=s" => \$hdeltlst,
	   "b=s" => \$stylebdy,
	   "i+" =>  \$didxflag) or
  die $usagetext;

if ($helpflag) {
  print $usagetext;
  exit 0;
}

if ($vrsnflag) {
  print "XNav version $xnversion\n";
  exit 0;
}

# Check -c and -b flag parameters
die $usagetext if (!(((!defined $hdeltlst) or ($hdeltlst =~ /^all|none$/) or
      ($hdeltlst =~ /^(title|base|script|style|meta|link|object)
                      (,(title|base|script|style|meta|link|object))*$/x)) and
      ((!defined $stylebdy) or ($stylebdy =~ /^rmv|div$/))));

my $xnavcmd = @ARGV[0];
my $xnavpath = @ARGV[1];

# Deal with requested command
my $cmdsel = {'init' => \&xnavinit,
	      'make' => \&xnavmake,
	      'valid' => \&xnavvalid,
	      'clean' => \&xnavclean};
if (! defined $cmdsel->{$xnavcmd}) {
  warn $usagetext;
  die "xnav: \'$xnavcmd\' is not a valid command\n";
}
if (! -d $xnavpath) {
  warn $usagetext;
  die "xnav: \'$xnavpath\' is not a valid path\n";
}

# Set environment variable used by xml tools
if (-r '/etc/xml/catalog' and $ENV{'XML_CATALOG_FILES'} eq '') {
  $ENV{'XML_CATALOG_FILES'} = "/etc/xml/catalog";
}
$ENV{'XML_CATALOG_FILES'} = "$xnavpath/XNAV/catalog.xml " .
                            $ENV{'XML_CATALOG_FILES'};
undef $ENV{'SGML_CATALOG_FILES'};

my $status = $cmdsel->{$xnavcmd}->($xnavpath);

exit $status;

#########################################################################

sub xnavinit {
  my $xnavpath = shift;

  my $xnavdir = "$xnavpath/XNAV";
  my $localxnav = <<"EOF";
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                version="1.0">

  <xsl:include href="$xmlpath/xnav.xsl"/>

</xsl:stylesheet>
EOF
  if ( -d $xnavdir ) {
    die "xnav: $xnavpath has already been initialised\n";
  } else {
    system("cp $incpath/*.css $xnavpath");
    mkdir $xnavdir;
    system("cp $incpath/*.xml $xnavdir");
    system("cp $xmlpath/catalog.xml $xnavdir");
    open(LXNAV, "> $xnavdir/xnavinc.xsl");
    print LXNAV $localxnav;
    close(LXNAV);
  }
  return 0;
}

#########################################################################

sub xnavmake {
  my $xnavpath = shift;

  $xnavpath = "$xnavpath/" if ($xnavpath !~ /\/$/);
  my $xnavdir = "${xnavpath}XNAV";
  rootpathcheck($xnavpath);
  my $hdtitle = ($hdeltlst eq 'all' or $hdeltlst =~ /title/)?'1':'0';
  my $hdbase = ($hdeltlst eq 'all' or $hdeltlst =~ /base/)?'1':'0';
  my $hdscript = ($hdeltlst eq 'all' or $hdeltlst =~ /script/)?'1':'0';
  my $hdstyle = ($hdeltlst eq 'all' or $hdeltlst =~ /style/)?'1':'0';
  my $hdmeta = ($hdeltlst eq 'all' or $hdeltlst =~ /meta/)?'1':'0';
  my $hdlink = ($hdeltlst eq 'all' or $hdeltlst =~ /link/)?'1':'0';
  my $hdobject = ($hdeltlst eq 'all' or $hdeltlst =~ /object/)?'1':'0';
  my $stbody = ($stylebdy eq 'rmv' or $stylebdy eq 'div')?'0':'1';
  my $stbdiv = ($stylebdy eq 'div')?'1':'0';
  my $mkcmd = "$xsltproc --novalid --stringparam stylep xnav.css " .
              "          --stringparam headp XNAV/head.xml " .
	      "          --stringparam footp XNAV/foot.xml " .
	      "          --stringparam basep $xnavpath " .
              "          --stringparam verbose $vrbsflag " .
              "          --stringparam dpindex $didxflag " .
	      "          --stringparam hdtitle $hdtitle " .
	      "          --stringparam hdbase $hdbase " .
	      "          --stringparam hdscript $hdscript " .
	      "          --stringparam hdstyle $hdstyle " .
	      "          --stringparam hdmeta $hdmeta " .
	      "          --stringparam hdlink $hdlink " .
	      "          --stringparam hdobject $hdobject " .
              "          --stringparam stbody $stbody " .
              "          --stringparam stbdiv $stbdiv " .
	      "          $xnavdir/xnavinc.xsl $xnavpath/xnav.xml";
  system($mkcmd) == 0 or die "xnav: system call to xsltproc failed\n";
  return 0;
}

#########################################################################

sub xnavvalid {
  my $xnavpath = shift;

  my $vldflg = 1;
  $xnavpath = "$xnavpath/" if ($xnavpath !~ /\/$/);
  rootpathcheck($xnavpath);
  my $srclist = sourcelist($xnavpath);
  my ($s, $p, $vflg, $cmd, $rslt, $xlerr, $smsg, $fmsg);
  foreach $s ( sort @$srclist ) {
    if ($vrbsflag > 0) {
      $p = $s;
      $p =~ s/$xnavpath//;
      $p = "[XNav Root]/$p" if ($p !~ /\//);
      print "Checking: $p "
    }
    if (-r $s) {
      $vflg = `grep '<!DOCTYPE' $s` ne '';
      $cmd = "$xmllint --noout ".($vflg?'--valid':'')." $s";
      $rslt = `$cmd 2>&1`;
      $xlerr = $? >> 8;
      die "xnav: system call to xmllint failed\n"
	if ($xlerr != 0 and $xlerr != 3 and $xlerr != 4);
      $smsg = $vflg?'[valid]':'[well formed]';
      $fmsg = $vflg?'[invalid]':'[not well formed]';
    } else {
      $fmsg = '[file not readable]';
      $rslt = "$s: could not open file for reading\n";
      $xlerr = 1;
    }
    if ($xlerr == 0) {
      print "$smsg\n" if ($vrbsflag > 0);
    } else {
      print "$fmsg\n" if ($vrbsflag > 0);
      print $rslt;
      $vldflg = 0;
    }
  }
  return ($vldflg)?0:1;
}

#########################################################################

sub xnavclean {
  my $xnavpath = shift;

  $xnavpath = "$xnavpath/" if ($xnavpath !~ /\/$/);
  rootpathcheck($xnavpath);
  my ($deplist, $status) = dependlist($xnavpath);
  die "xnav: could not determine dependencies due to xnav.xml parse error\n"
    if ($status != 0);
  print "Cleaning: " if ($vrbsflag > 0);
  my ($d, $s, $f);
  foreach $d ( keys %$deplist ) {
    $f = 1;
    foreach $s ( @{$deplist->{$d}} ) {
      if ( ! -f $s ) {
	$f = 0;
	last;
      }
    }
    if ($f and -f $d) {
      unlink $d;
       print "$d " if ($vrbsflag > 0)
    }
  }
  print "\n" if ($vrbsflag > 0);
  return 0;
}

#########################################################################

sub rootpathcheck {
  my $xnavpath = shift;

  if ( ! -d "${xnavpath}XNAV" ) {
    warn $usagetext;
    die "xnav: $xnavpath has not been initialised\n";
  }
  if ( ! -f "${xnavpath}XNAV/xnavinc.xsl" ) {
    warn $usagetext;
    die "xnav: local xnav xsl not found in ${xnavpath}XNAV\n";
  }
  if ( ! -f "${xnavpath}XNAV/catalog.xml" ) {
    warn $usagetext;
    die "xnav: local xml catalog not found in ${xnavpath}XNAV\n";
  }
  if ( ! -f "${xnavpath}xnav.xml" ) {
    warn $usagetext;
    die "xnav: top level config file xnav.xml not found in $xnavpath\n";
  }
  return 0;
}

#########################################################################

sub dependlist {
  my $xnavpath = shift;

  my $dpcmd = "$xsltproc --novalid --stringparam basep $xnavpath " .
	      "          $xmlpath/dpnd.xsl ${xnavpath}xnav.xml";
  my $dptxt = `$dpcmd 2>/dev/null`;
  my $xperr = $? >> 8;
  die "xnav: system call to xsltproc failed\n"
    if ($xperr != 0 and $xperr != 6 and $xperr != 10);
  my ($line, $dst);
  my $dplist = {};
  while ($dptxt =~ /(.*)\n/) {
    $line = $1;
    $dptxt = $';
    if ($line =~ /([^\s]+)\s*/) {
      $dst = $1;
      $line = $';
      $dplist->{$dst} = [];
      while ($line =~ /([^\s]+)\s*/) {
	push @{$dplist->{$dst}}, $1;
	$line = $';
      }
    }
  }
  # Status set to 0 on success, 1 on failure reading top level
  # xnav.xml file, and 2 on failure reading other xnav.xml file
  my $status = 0;
  if ($xperr != 0) {
    $status = ($xperr == 6)?1:2;
  }
  return ($dplist, $status);
}

#########################################################################

sub sourcelist {
  my $xnavpath = shift;

  my ($deplist, $status) = dependlist($xnavpath);
  my $srchash = {};
  $srchash->{"$xnavpath/xnav.xml"} = 1; # Might be missing if dependlist fails
  my ($d, $s);
  foreach $d ( keys %$deplist ) {
    foreach $s ( @{$deplist->{$d}} ) {
      $srchash->{$s} = 1;
    }
  }
  return [ keys %$srchash ];
}

#########################################################################

__END__

=pod

=head1 NAME

xnav - Construct navigation top and sidebars for a collection of web pages

=head1 SYNOPSIS

B<xnav> [ B<-V> ] [ B<-h> ] [ B<-v> ] [ B<-c> (all | (title | base | script | style | meta | link | object)[,(title | base | script | style | meta | link | object)][,...])] [ B<-b> (rmv | div) ] [ B<-i> ] [ B<init> | B<make> | B<valid> | B<clean> ] I<path>

=head1 DESCRIPTION

B<XNav> adds a wrapper with CSS style and top and side navigation bars
to a directory tree of XHTML documents. It also provides a mechanism
for automatic construction of XHTML documents from user defined XML
document types, using user suplied XSL.

=head1 OPTIONS

=over 4

=item B<init> I<path>

Initialise directory I<path> as an XNav website.

=item B<make> I<path>

Construct the XNav website in directory I<path>.

=item B<valid> I<path>

Validate XML and XHTML in directory I<path>.

=item B<clean> I<path>

Clean auto-generated files in directory I<path>.

=item B<-V>

Display version.

=item B<-h>

Display usage information.

=item B<-v>

Verbose operation.

=item B<-c>

Specify the html/head children to include in the output
documents. Allowed values are 'all' (the default), 'none', or a comma
separated list of element names 'title', 'base', 'script', 'style',
'meta', 'link', and 'object'.

=item B<-b>

Select handling of the body specification in CSS within a
html/head/style element. Valid values are 'rmv', requesting removal of
any body definitions, and 'div', requesting replacement by a top level
div element with the same style.

=item B<-i>

Append 'index.html' to top and side navigation directory
paths. Primarily useful for constructing a set of pages that may be
navigated as files accessed directly by a web browser, rather than
through a web server.

=back

=head1 USAGE

Start by setting up the desired directory structure of the collection
of web pages. All HTML documents should be valid XHTML 1.0 Strict, and
should have extension .xml rather than the usual .html. Each directory
should have, at least, an index document called F<index.xml>, and a
configuration file called F<xnav.xml>.

The configuration file format is defined in the file F<xnav.dtd>. The
top level element is B<xnav>, with optional child elements
B<directory> and B<file>. The label for a specific directory is
usually provided by the label in the referencing directory element in
the parent directory, but may be specified by using the B<label>
attribute in the top level directory. If the F<index.xml> file is not
HTML, the B<type> attribute should be set to the name of the document
format, and an XSL template should be provided for handling that type.

A B<directory> element represents a navigation link to a subdirectory,
the actual directory name being specified by the B<href> attribute,
and the directory label used in the navigation bars being specified as
the element content. If a directory element points to a directory not
managed by XNav (i.e. into which the build script should not recurse),
the B<enter> attribute value should be 'no'. If the directory is
managed by XNav, but is for some reason desired to be excluded from
the side navigation bar, the B<sidenav> attribute value should be
'no'.

A B<file> element represents a link to a file within the same
directory as the xnav.xml file, and has similar usage to the directory
element. If the file is not HTML, the B<type> attribute should be set
to the name of the document format, and an XSL template should be
provided for handling that type. If the file is desired to be excluded
from the side navigation bar, the B<sidenav> attribute value should be
'no'.

Initialise the directory structure at I<path> using the
command

=over

B<xnav init> I<path>

=back

creating a directory B<XNAV> in I<path>, containing configuration
files which may be edited by the user. The files F<head.xml> and
F<foot.xml> define header and footer HTML added to every page
generated by XNav. The F<catalog.xml> file allows the XML processing
utilities used by XNav to locate the DTD for the F<xnav.xml>
configuration files. If the user adds additional document types for
processing by XNav, references to the relevant DTDs should be added to
the catalog file. Finally, the F<xnavinc.xsl> file includes the main
XSL stylesheet F<xnav.xsl> responsible for the majority of XNav
processing.

XNav can be extended to process arbitrary XML document types by
including additional XSL stylesheets within the F<xnavinc.xsl>
file. If a non-HTML document types is referred to as I<newtype> in the
B<type> attribute, the user should define an XSL template with name
I<newtype> and mode B<dynamic-template-select>. Within this template,
the content of the document to be processed is available at XPath
B<src/>I<newtype>.

Once initialisation is complete, and whenever source XML documents are
edited, the output HTML files may be generated using the command

=over

B<xnav make> I<path>

=back

The command

=over

B<xnav valid> I<path>

=back

validates source XML files which contain a B<DOCTYPE>
specification. Files that do not contain such a specification are
checked to determine whether they are well formed.


=head1 AUTHOR

Brendt Wohlberg <software@wohlberg.net>


=head1 COPYRIGHT

Copyright © 2003-2008 Brendt Wohlberg <software@wohlberg.net>

This program is free software; you can redistribute it and/or modify
it under the terms of version 2 of the GNU General Public License
L<http://www.gnu.org/licenses/gpl-2.0.txt>.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.


=head1 AVAILABILITY

Available from L<http://www.wohlberg.net/public/software/xml/xnav/>


=cut

#########################################################################
