The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package App::XML::DocBook::Docmake;

use strict;
use warnings;

use 5.008;

use Getopt::Long qw(GetOptionsFromArray);
use File::Path;
use Pod::Usage;

use parent 'Class::Accessor';

=head1 NAME

App::XML::DocBook::Docmake - translate DocBook/XML to other formats

=head1 VERSION

Version 0.0403

=cut

use vars qw($VERSION);

$VERSION = '0.0403';

__PACKAGE__->mk_accessors(qw(
    _base_path
    _has_output
    _input_path
    _make_like
    _mode
    _output_path
    _stylesheet
    _verbose
    _real_mode
    _xslt_mode
    _xslt_stringparams
));

=head1 SYNOPSIS

    use App::XML::DocBook::Docmake;

    my $docmake = App::XML::DocBook::Docmake->new({argv => [@ARGV]});

    $docmake->run()

=head1 FUNCTIONS

=head2 my $obj = App::XML::DocBook::Docmake->new({argv => [@ARGV]})

Instantiates a new object.

=cut

my %modes =
(
    'fo' =>
    {
    },
    'help' =>
    {
        standalone => 1,
    },
    'xhtml' =>
    {
    },
    'xhtml-1_1' =>
    {
        real_mode => "xhtml",
    },
    'rtf' =>
    {
        xslt_mode => "fo",
    },
    'pdf' =>
    {
        xslt_mode => "fo",
    },
);

sub new
{
    my $class = shift;
    my $self = {};

    bless $self, $class;

    $self->_init(@_);

    return $self;
}

sub _init
{
    my ($self, $args) = @_;

    my $argv = $args->{'argv'};

    my $output_path;
    my $verbose = 0;
    my $stylesheet;
    my @in_stringparams;
    my $base_path;
    my $make_like = 0;
    my ($help, $man);

    my $ret = GetOptionsFromArray($argv,
        "o=s" => \$output_path,
        "v|verbose" => \$verbose,
        "x|stylesheet=s" => \$stylesheet,
        "stringparam=s" => \@in_stringparams,
        "basepath=s" => \$base_path,
        "make" => \$make_like,
        'help|h' => \$help,
        'man' => \$man,
    );

    if (!$ret)
    {
        pod2usage(2);
    }
    if ($help)
    {
        pod2usage(1);
    }
    if ($man)
    {
        pod2usage(-exitstatus => 0, -verbose => 2)
    }

    my @stringparams;
    foreach my $param (@in_stringparams)
    {
        if ($param =~ m{\A([^=]+)=(.*)\z}ms)
        {
            push @stringparams, [$1,$2];
        }
        else
        {
            die "Wrong stringparam argument '$param'! Does not contain a '='!";
        }
    }

    $self->_has_output(
        $self->_output_path($output_path) ? 1 : 0
    );


    $self->_verbose($verbose);
    $self->_stylesheet($stylesheet);
    $self->_xslt_stringparams(\@stringparams);
    $self->_make_like($make_like);
    $self->_base_path($base_path);

    my $mode = shift(@$argv);

    my $mode_struct = $modes{$mode};

    if ($mode_struct)
    {
        $self->_mode($mode);

        my $assign_secondary_mode = sub {
            my ($struct_field, $attr) = @_;
            $self->$attr($mode_struct->{$struct_field} || $mode);
        };

        $assign_secondary_mode->('real_mode', '_real_mode');
        $assign_secondary_mode->('xslt_mode', '_xslt_mode');
    }
    else
    {
        die "Unknown mode \"$mode\"";
    }

    my $input_path = shift(@$argv);

    if (! (defined($input_path) || $mode_struct->{standalone}) )
    {
        die "Input path not specified on command line";
    }
    else
    {
        $self->_input_path($input_path);
    }

    return;
}

=head2 $docmake->run()

Runs the object.

=cut

sub _exec_command
{
    my ($self, $cmd) = @_;

    if ($self->_verbose())
    {
        print (join(" ", @$cmd), "\n");
    }

    if (system(@$cmd)) {
        die qq/<<@$cmd>> failed./;
    }

    return 0;
}

sub run
{
    my $self = shift;

    my $real_mode = $self->_real_mode();

    my $mode_func = '_run_mode_' . $self->_real_mode;

    return $self->$mode_func(@_);
}

sub _run_mode_help
{
    my $self = shift;

    print <<"EOF";
Docmake version $VERSION
A tool to convert DocBook/XML to other formats

Available commands:

    help - this help screen.

    fo - convert to XSL-FO.
    rtf - convert to RTF (MS Word).
    pdf - convert to PDF (Adobe Acrobat).
    xhtml - convert to XHTML.
    xhtml-1_1 - convert to XHTML-1.1.
EOF
}

sub _is_older
{
    my $self = shift;

    my $file1 = shift;
    my $file2 = shift;

    my @stat1 = stat($file1);
    my @stat2 = stat($file2);

    if (! @stat2)
    {
        die "Input file '$file1' does not exist.";
    }
    elsif (! @stat1)
    {
        return 1;
    }
    else
    {
        return ($stat1[9] <= $stat2[9]);
    }
}

sub _should_update_output
{
    my $self = shift;
    my $args = shift;

    return $self->_is_older($args->{output}, $args->{input});
}

sub _run_mode_fo
{
    my $self = shift;
    return $self->_run_xslt();
}

sub _mkdir
{
    my ($self, $dir) = @_;

    mkpath($dir);
}

sub _run_mode_xhtml
{
    my $self = shift;

    # Create the directory, because xsltproc requires it.
    $self->_mkdir($self->_output_path());

    return $self->_run_xslt();
}

sub _calc_default_xslt_stylesheet
{
    my $self = shift;

    my $mode = $self->_xslt_mode();

    return
        "http://docbook.sourceforge.net/release/xsl/current/${mode}/docbook.xsl"
        ;
}

sub _is_xhtml
{
    my $self = shift;

    return (($self->_mode() eq "xhtml") || ($self->_mode() eq "xhtml-1_1"));
}

sub _calc_output_param_for_xslt
{
    my $self = shift;
    my $args = shift;

    my $output_path = $self->_output_path();
    if (defined($args->{output_path}))
    {
        $output_path = $args->{output_path};
    }

    if (!defined($output_path))
    {
        die "Output path not specified!";
    }

    # If it's XHTML, then it's a directory and xsltproc requires that
    # it will have a trailing slash.
    if ($self->_is_xhtml)
    {
        if ($output_path !~ m{/\z})
        {
            $output_path .= "/";
        }
    }

    return $output_path;
}

sub _calc_make_output_param_for_xslt
{
    my $self = shift;
    my $args = shift;

    my $output_path = $self->_calc_output_param_for_xslt($args);

    # If it's XHTML, then we need to compare against the index.html
    # because the directory is freshly made.
    if ($self->_is_xhtml)
    {
        $output_path .= "index.html";
    }

    return $output_path;
}

sub _pre_proc_command
{
    my ($self, $args) = @_;

    my $input_file = $args->{input};
    my $output_file = $args->{output};
    my $template = $args->{template};

    return
    [
        map
        {
              (ref($_) eq '') ? $_
            : $_->is_output() ? $output_file
            : $_->is_input() ? $input_file
            # Not supposed to happen
            : do { die "Unknown Argument in Command Template."; }
        } @$template
    ];
}

sub _run_input_output_cmd
{
    my $self = shift;
    my $args = shift;

    my $input_file = $args->{input};
    my $output_file = $args->{output};
    my $make_output_file = $args->{make_output};

    if (!defined($make_output_file))
    {
        $make_output_file = $output_file;
    }

    if (
        (!$self->_make_like())
            ||
        $self->_should_update_output(
            {
                input => $input_file,
                output => $make_output_file,
            }
        )
    )
    {
        $self->_exec_command(
            $self->_pre_proc_command($args),
        );
    }
}

sub _on_output
{
    my ($self, $meth, $args) = @_;

    return $self->_has_output() ? $self->$meth($args) : ();
}

sub _calc_output_params
{
    my ($self,$args) = @_;

    return
    (
        output => $self->_calc_output_param_for_xslt($args),
        make_output => $self->_calc_make_output_param_for_xslt($args),
    );
}

sub _calc_template_o_flag
{
    my ($self,$args) = @_;

    return ("-o", $self->_output_cmd_comp());
}

sub _calc_template_string_params
{
    my ($self) = @_;

    return [map { ("--stringparam", @$_ ) } @{$self->_xslt_stringparams()}];
}

sub _run_xslt
{
    my $self = shift;
    my $args = shift;

    my @stylesheet_params = ($self->_calc_default_xslt_stylesheet());

    if (defined($self->_stylesheet()))
    {
        @stylesheet_params = ($self->_stylesheet());
    }

    my @base_path_params = ();

    if (defined($self->_base_path()))
    {
        @base_path_params =
        (
            "--path",
            ($self->_base_path() . '/' . $self->_xslt_mode()),
        );
    }

    return $self->_run_input_output_cmd(
        {
            input => $self->_input_path(),
            $self->_on_output('_calc_output_params', $args),
            template =>
            [
                "xsltproc",
                $self->_on_output('_calc_template_o_flag', $args),
                @{$self->_calc_template_string_params()},
                @base_path_params,
                @stylesheet_params,
                $self->_input_cmd_comp(),
            ],
        },
    );
}

sub _run_xslt_and_from_fo
{
    my $self = shift;
    my $args = shift;

    my $xslt_output_path = $self->_output_path();

    # TODO : do something meaningful if a period (".") is not present
    if ($xslt_output_path !~ m{\.}ms)
    {
        $xslt_output_path .= ".fo";
    }
    else
    {
        $xslt_output_path =~ s{\.([^\.]*)\z}{\.fo}ms;
    }

    $self->_run_xslt({output_path => $xslt_output_path});

    return $self->_run_input_output_cmd(
        {
            input => $xslt_output_path,
            output => $self->_output_path(),
            template =>
            [
                "fop",
                ("-".$args->{fo_out_format}),
                $self->_output_cmd_comp(),
                $self->_input_cmd_comp(),
            ],
        },
    );
}

sub _run_mode_pdf
{
    my $self = shift;

    return $self->_run_xslt_and_from_fo(
        {
            fo_out_format => "pdf",
        },
    );
}

sub _run_mode_rtf
{
    my $self = shift;

    return $self->_run_xslt_and_from_fo(
        {
            fo_out_format => "rtf",
        },
    );
}

sub _input_cmd_comp
{
    my $self = shift;

    return App::XML::DocBook::Docmake::CmdComponent->new(
        {
            is_input => 1,
            is_output => 0,
        }
    );
}

sub _output_cmd_comp
{
    my $self = shift;

    return App::XML::DocBook::Docmake::CmdComponent->new(
        {
            is_input => 0,
            is_output => 1,
        }
    );
}

package App::XML::DocBook::Docmake::CmdComponent;

use base 'Class::Accessor';

__PACKAGE__->mk_accessors(qw(
    is_input
    is_output
    ));

1;

=head1 AUTHOR

Shlomi Fish, C<< <shlomif at cpan.org> >>

=head1 BUGS

Please report any bugs or feature requests to
C<bug-app-docbook-xml-docmake at rt.cpan.org>, or through the web interface at
L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=App::XML::DocBook::Docmake>.
I will be notified, and then you'll automatically be notified of progress on
your bug as I make changes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc App::XML::DocBook::Docmake

You can also look for information at:

=over 4

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/App::XML::DocBook::Docmake>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/App::XML::DocBook::Docmake>

=item * RT: CPAN's request tracker

L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=App::XML::DocBook::Docmake>

=item * Search CPAN

L<http://search.cpan.org/dist/App::XML::DocBook::Docmake>

=back

=head1 ACKNOWLEDGEMENTS

=head1 COPYRIGHT & LICENSE

Copyright 2008 Shlomi Fish.

This program is released under the following license: MIT/X11 License.
( L<http://www.opensource.org/licenses/mit-license.php> ).

=cut

1;