package Lire::ReportParser::LaTeXDocBookFormatter;

use strict;

use base qw/ Exporter Lire::DocBookParser /;

use Lire::Utils qw/deep_copy/;
use Lire::I18N qw/ensure_utf8/;

our @EXPORT_OK = qw/ dbk2latex /;

=pod

=head1 NAME

Lire::ReportParser::LaTeXDocBookFormatter - Lire::DocBookParser subclass
which formats description to LaTeX.

=head1 SYNOPSIS

To convert a DocBook decription to LaTeX:

    use Lire::ReportParser::LaTeXDocBookFormatter qw/dbk2latex/;

    my $LaTeX = dbk2latex( "<para>Test</para>" );

=head1 DESCRIPTION

This package implements methods that can handle the content of
C<description> elements and it can be used by a subclass of
Lire::ReportParser. Client only have to inherit from this module so
that a handle_description() method is available to process the text
formatted DocBook description.

This module also provide a convenient dbk2txt() function which can be
used to format a string containing DocBook elements into an ASCII
equivalent.

=head1 USING Lire::ReportParser::LaTeXDocBookFormatter

Lire::ReportParser processors that would like to work with text
version of the description should inherit from
Lire::ReportParser::LaTeXDocBookFormatter in addition to
Lire::ReportParser. If they override the description_start(),
description_end()methods, they B<must> link to their parents' version
using C<SUPER::>.

Additionnally, they should merge the value elements_spec() in their
elements_spec() implementation.

The Lire::ReportParser::LaTeXDocBookFormatter should be listed before
Lire::ReportParser in the @ISA. The LaTeXDocBookFormatter doesn't inherit
directly from Lire::ReportParser so that it can be used in multiple
inheritance scenario.

=cut

sub new {
    my $self = shift->SUPER::new( @_ );

    return $self;
}

=pod

=head2 dbk_start_processing()

Initializes the parser's structure for formatting DocBook XML as ASCII.
This is used from the description_start implementation.

=cut

sub dbk_start_processing {
    my $self = $_[0];

    $self->{'dbk_process'} = 1;
    $self->init_stack( 'latex' );
    $self->stack_push( 'latex', Lire::LaTeX::Node->new() );

    return;
}

=pod

=head2 dbk_end_processing()

Cleans the parser structure. This is used from the description_end() 
implementation.

=cut

sub dbk_end_processing {
    my $self = $_[0];

    delete $self->{'dbk_process'};
    die "stack 'latex' should be empty"
      unless $self->is_stack_empty( 'latex' );

    return;
}

sub namespaces {
    my $self = $_[0];

    return { "http://www.logreport.org/LRML/" => 'lire' };
}

sub elements_spec {
    my $self = $_[0];

    my $spec = deep_copy( $self->Lire::DocBookParser::elements_spec() );
    foreach my $element ( keys %$spec ) {
        $spec->{$element}{'start'} = 'latex_start';
        $spec->{$element}{'end'} = 'latex_end';
        $spec->{$element}{'char'} = 'latex_char'
          if exists $spec->{$element}{'char'};
    }

    $spec->{'lire:description'} = [ @Lire::DocBookParser::top_levels ];

    return $spec;
}

sub description_start {
    $_[0]->dbk_start_processing();

    return;
}

sub description_end {
    my $self = $_[0];

    return unless $self->{'dbk_process'};

    my $root = $self->stack_pop( 'latex' );
    $self->handle_description( $root->as_string() );
    $self->dbk_end_processing();

    return;
}

=pod

=head2 handle_description( $description )

This method is invoked after the closing tag of the C<description>
element is encountered. The $description contains the description
converter to LaTeX.

=cut

sub handle_description {
    $_[0]{'saved_latex'} = $_[1];

    return;
}

sub parse_end {
    return $_[0]{'saved_latex'};
}

my %dbk2latex = ( 'para' => 'Para',

                  'emphasis' => 'Emph',
                  'foreignphrase' => 'Emph',

                  'ulink' => 'Href',
                  'email' => 'Email',

                  'superscript' => 'Superscript',
                  'subscript' => 'Subscript',

                  'optional' => 'Slanted',
                  'parameter' => 'Slanted',
                  'varname' => 'Slanted',

                  'command' => 'TextTT',
                  'constant' => 'TextTT',
                  'computeroutput' => 'TextTT',
                  'filename' => 'TextTT',
                  'userinput' => 'TextTT',

                  'quote' => 'Quote',

                  'itemizedlist' => 'List',
                  'orderedlist' => 'List',
                  'variablelist' => 'List',

                  'listitem' => 'Item',
                  'varlistentry' => 'NamedItem',
                  'term' => 'Term',
                  'title' => 'Title',

                  'warning' => 'Admonition',
                  'caution' => 'Admonition',
                  'important' => 'Admonition',
                  'note' => 'Admonition',
                  'tip' => 'Admonition',
                );

sub latex_start {
    my ( $self, $name, $attr ) = @_;

    my $class = "Lire::LaTeX::" . ( $dbk2latex{$name} || 'Node' );
    my $node = $class->new( $name, $attr );
    $self->stack_peek( 'latex' )->add_child( $node );
    $self->stack_push( 'latex', $node );

    return;
}

sub latex_end {
    my ( $self, $name ) = @_;

    $self->stack_pop( 'latex' );

    return;
}

sub latex_char {
    my ( $self, $text ) = @_;

    $self->stack_peek( 'latex' )->add_child( Lire::LaTeX::Text->new( $text ) );

    return;
}

=pod

=head1 PROCESSING LaTeX DESCRIPTION

The generate LaTeX is encoded in UTF-8, so Omega must be used to
process it. Additionnally, the result of calling
dbk_latex_environment() should be added to the document preambule. It
adds some environment and command definitions used by the formatter.

=head1 FORMATTING DocBook STRINGS

If you have DocBook content in a string, like you can obtain from some
of the Report Specifications object, you can convert it to LaTeX by
using the dbx2latex() function.

=cut

sub dbk_latex_environment {
    return <<'EOF';
EOF

}

=pod

=head2 dbk2txt( $docbook_str, [$columns] )

Returns a plain text version of the DocBook XML fragment $docbook_str. The
C<columns> parameter sets the number of columns in which the DocBook text
should be formatted.

This method will die() in case of error.

=cut

sub dbk2latex {
    my $docbook_str = $_[0];

    my $parser =
      new Lire::ReportParser::LaTeXDocBookFormatter();

    return $parser->parse( '<?xml version="1.0" encoding="utf-8"?>'
                           . '<lire:description xmlns:lire='
                           . '"http://www.logreport.org/LRML/">'
                           . ensure_utf8( $docbook_str )
                           . '</lire:description>' );
}

package Lire::LaTeX::Node;

sub new {
    return bless { 'children' => [] }, shift;
}

sub add_child {
    my ( $self, $child ) = @_;

    push @{$self->{'children'}}, $child;

    return;
}

sub as_string {
    my $self = $_[0];

    my $str = '';
    foreach my $child ( @{$self->{'children'}} ) {
        $str .= $child->as_string();
    }

    return $str;
}

package Lire::LaTeX::Displayed;

use base qw/Lire::LaTeX::Node/;

package Lire::LaTeX::Inlined;

use base qw/Lire::LaTeX::Node/;

package Lire::LaTeX::Para;

use base qw/Lire::LaTeX::Displayed/;

use Text::Wrap;

sub as_string {
    my $self = $_[0];

    local $Text::Wrap::columns = 72;

    my $str = '';
    my $run = '';
    foreach my $child ( @{$self->{'children'}} ) {
        if ( $child->isa( 'Lire::LaTeX::Displayed' ) ) {
            $str .= $self->wrap_run( $run, length $str );
            $str .= $child->as_string();
            $run = '';
        } else {
            $run .= $child->as_string();
        }
    }
    $str .= $self->wrap_run( $run, length $str );

    return $str;
}

sub wrap_run {
    my ( $self, $run, $noindent ) = @_;

    return '' unless length $run;
    $run =~ s/\s+/ /g;
    return ( $noindent ? "\\noindent\n" : '' ) . wrap( '', '', $run ) . "\n\n";
}

package Lire::LaTeX::Emph;

use base qw/Lire::LaTeX::Inlined/;

sub as_string {
    return '\emph{' . shift->SUPER::as_string() . '}';
}

package Lire::LaTeX::Slanted;

use base qw/Lire::LaTeX::Inlined/;

sub as_string {
    return '\textsl{' . shift->SUPER::as_string() . '}';
}

package Lire::LaTeX::TextTT;

use base qw/Lire::LaTeX::Inlined/;

sub as_string {
    return '\texttt{' . shift->SUPER::as_string() . '}';
}

package Lire::LaTeX::Subscript;

use base qw/Lire::LaTeX::Inlined/;

sub as_string {
    return '\raisebox{-1ex}{' . shift->SUPER::as_string() . '}';
}

package Lire::LaTeX::Superscript;

use base qw/Lire::LaTeX::Inlined/;

sub as_string {
    return '\raisebox{1ex}{' . shift->SUPER::as_string() . '}';
}

package Lire::LaTeX::Quote;

use base qw/Lire::LaTeX::Inlined/;

sub as_string {
    return '``' . shift->SUPER::as_string() . "''";
}

package Lire::LaTeX::Href;

use base qw/Lire::LaTeX::Inlined/;

sub new {
    my ( $class, $name, $attr ) = @_;

    my $self = $class->SUPER::new( $attr );

    $self->{'href'} = $attr->{'url'};

    return $self;
}

sub as_string {
    my $self = $_[0];

    return '\href{' . $self->{'href'} . '}{' . $self->SUPER::as_string() . '}';
}

package Lire::LaTeX::Email;

use base qw/Lire::LaTeX::Inlined/;

sub as_string {
    my $self = $_[0];

    my $email = $self->SUPER::as_string();
    return "\\href{mailto:$email}{$email}";
}

package Lire::LaTeX::Text;

use base qw/Lire::LaTeX::Inlined/;

use Lire::Utils qw/latex_encode/;

sub new {
    my ( $pkg, $content ) = @_;

    return bless \$content, $pkg;
}

sub as_string {
    my $self = $_[0];

    return latex_encode( $$self );
}

package Lire::LaTeX::Admonition;

use base qw/Lire::LaTeX::Displayed/;

use Locale::TextDomain 'lire';

my %admonition_titles = ( 'note' => N__( 'Note:' ),
                          'warning' => N__( 'Warning:' ),
                          'tip' => N__( 'Tip:' ),
                          'caution' => N__( 'Caution:' ),
                          'important' => N__( 'Important:' ),
                        );

sub new {
    my ( $class, $name, $attr ) = @_;

    my $self = $class->SUPER::new();
    $self->{'title'} = Lire::LaTeX::Title->new();
    $self->{'title'}->add_child( Lire::LaTeX::Text->new( $__{$admonition_titles{$name}} ) );

    return $self;
}

sub add_child {
    my ( $self, $child ) = @_;

    if ( $child->isa( 'Lire::LaTeX::Title' ) ) {
        $self->{'title'} = $child;
    } else {
        $self->SUPER::add_child( $child );
    }
}

sub as_string {
    my $self = $_[0];

    return join( "", "\\begin{quote}\n",
                 $self->{'title'}->as_string(), "\n",
                 $self->SUPER::as_string(),
                 "\\end{quote}\n\n" );
}

package Lire::LaTeX::List;

use base qw/Lire::LaTeX::Displayed/;

my %dbk2latex_list = ( 'itemizedlist' => 'itemize',
                       'orderedlist' => 'enumerate',
                       'variablelist' => 'description',
                     );

sub new {
    my ( $class, $name, $attr ) = @_;

    my $self = bless { 'title' => undef,
                       'preamble' => [],
                       'type' => $dbk2latex_list{$name},
                       'items' => [] }, $class;

    return $self;
}

sub add_child {
    my ( $self, $child ) = @_;

    if ( $child->isa( 'Lire::LaTeX::Title' ) ) {
        $self->{'title'} = $child;
    } elsif ( $child->isa( 'Lire::LaTeX::Item' ) ) {
        push @{$self->{'items'}}, $child;
    } else {
        push @{$self->{'preamble'}}, $child;
    }
}

sub as_string {
    my $self = $_[0];

    my $str = '';
    if ( $self->{'title'} ) {
        $str .= $self->{'title'}->as_string() . "\n\n\\nopagebreak[3]\n\n";
    }

    if ( @{$self->{'preamble'}} ) {
        $str .= join( "", ( map { $_->as_string() } @{$self->{'preamble'}} ),
                      "\\nopagebreak[2]\n\n" );
    }

    $str .= join ( "", "\\begin{$self->{'type'}}\n",
                   ( map { $_->as_string() } @{$self->{'items'}} ),
                   "\\end{$self->{'type'}}\n\n" );

    return $str;
}

package Lire::LaTeX::Title;

use base qw/Lire::LaTeX::Inlined/;

sub as_string {
    return '\large{\textbf{' . shift->SUPER::as_string() . '}}';
}

package Lire::LaTeX::Item;

use base qw/Lire::LaTeX::Inlined/;

sub as_string {
    return '\item ' . shift->SUPER::as_string();
}

package Lire::LaTeX::NamedItem;

use base qw/Lire::LaTeX::Item/;

sub new {
    return bless { 'term' => undef,
                   'def' => undef }, shift;
}

sub add_child {
    my ( $self, $child ) = @_;

    if ( $child->isa( 'Lire::LaTeX::Term' ) ) {
        $self->{'term'} = $child;
    } else {
        $self->{'def'} = $child;
    }
}

sub as_string {
    my $self = $_[0];

    return $self->{'term'}->as_string() . ' '
      . $self->{'def'}->Lire::LaTeX::Node::as_string();
}

package Lire::LaTeX::Term;

use base qw/Lire::LaTeX::Inlined/;

sub as_string {
    return '\item[' . shift->SUPER::as_string() . ']';
}

1;

__END__


=head1 SEE ALSO

Lire::ReportParser(3pm)

=head1 VERSION

$Id: LaTeXDocBookFormatter.pm,v 1.4 2006/07/23 13:16:31 vanbaal Exp $

=head1 COPYRIGHT

Copyright (C) 2004 Stichting LogReport Foundation LogReport@LogReport.org

This file is part of Lire.

Lire is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

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.

You should have received a copy of the GNU General Public License
along with this program (see COPYING); if not, check with
http://www.gnu.org/copyleft/gpl.html. 

=head1 AUTHOR

Francis J. Lacoste <flacoste@logreport.org>

=cut
