The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

# = HISTORY SECTION =====================================================================

# ---------------------------------------------------------------------------------------
# version | date     | author   | changes
# ---------------------------------------------------------------------------------------
# 0.03    |05.07.2005| JSTENZEL | the meta data prolog is template driven now, thus
#         |          |          | allowing individual results;
#         |          | JSTENZEL | reactivated $docDate as now with the template approach
#         |          |          | it can be excluded from tests;
#         |          | JSTENZEL | new constructor option to suppress meta data handling
#         |          |          | completely;
#         |          | JSTENZEL | user defined document data keys now configured via
#         |          |          | constructor option;
#         |10.07.2005| JSTENZEL | oo2pp() starts result with two newlines now, to
#         |          |          | guarantee correct detection of the first paragraph;
# 0.02    |29.06.2005| JSTENZEL | new constructor option: imagebufferdir allows to set
#         |          |          | up a user defined name for the image buffer directory,
#         |          |          | which can be relative or absolute;
#         |          | JSTENZEL | added handling of external images;
#         |02.07.2005| JSTENZEL | bugfix: $docDate value was title, not date;
#         |          | JSTENZEL | checking if an image URL host is available, via Net::Ping;
#         |          | JSTENZEL | new user defined field support: authormail;
#         |03.07.2005| JSTENZEL | deactivated $docDate setting as it is difficult to handle
#         |          |          | with changing locales, suspended till all the variables
#         |          |          | can be set by options;
# 0.01    |19.06.2005| JSTENZEL | First version, parts derived or inspired from/by an
#         |          |          | oo2pod example in OpenOffice::OODoc 1.309 and modules
#         |          |          | in the OpenOffice::OODoc 2.00 distribution.
# ---------------------------------------------------------------------------------------

# = POD SECTION =========================================================================

=head1 NAME

B<OpenOffice::PerlPoint> - an Open Office / Open Document to PerlPoint converter class

=head1 VERSION

This manual describes version B<0.03>.

=head1 SYNOPSIS

 # load the module
 use OpenOffice::PerlPoint;

 # build an object
 my $oo2pp=new OpenOffice::PerlPoint(file=>$ooFile);

 # convert document
 my $perlpoint=$oo2pp->oo2pp;


=head1 DESCRIPTION

C<OpenOffice::PerlPoint> is a translator class to transform Open Office 1.0 and 2.0 (and
generally OASIS Open Document) documents into PerlPoint sources. It is based on
C<OpenOffice::OODoc>.

Once you have transformed an Open Office or Open Document document into PerlPoint, it may
be furtherly processed using the PerlPoint utilities.

If you prefer, you do not need to perform an explicit transformation. Beginning with
release 0.40, C<PerlPoint::Package> can process Open Office / Open Document sources directly.
Please see C<PerlPoint::Parser> for details, or the documentation that comes with PerlPoint.

B<This software is in alpha state. It supports just a I<subset> of the source format features.>
Please see the I<NOTES> sections below.

=head1 METHODS

=cut


# declare package
package OpenOffice::PerlPoint;

# declare version
$VERSION=0.03;

# pragmata
use strict;

# load modules
use Carp;
use Safe;
use Storable;
use Net::Ping;
use Text::Wrapper;
use File::Basename;
use LWP::UserAgent;
use Text::Template;
use POSIX qw(strftime);
use OpenOffice::OODoc 2.00;

# declare attributes
use fields qw(
              file
              archive

              metadata
              docContent
              docStyles
              content
              notes

              metaData

              userAgent
              ping

              skipmetadata
              imagebufferdir
              metadataTemplate
              userdefinedDocdata
             );


# define data: delimiter handling
my %delimiters=(
                'text:footnote-citation' => {
                                             begin => '[',
                                             end   => ']',
                                            },
                'text:footnote-body'     => {
                                             begin => '{NOTE: ',
                                             end   => '}',
                                            },
                'text:span'              => {
                                             begin => '<<',
                                             end   => '>>',
                                            },
                'text:list-item'         => {
                                             begin => '',
                                             end   => '',
                                            },
               );


# define data: style extraction directives for traversal (see traverseElement() below)
my @styles=(
            ['B',     'properties', 'fo:font-weight', 'bold', 0, '\B<', '>'],
            ['I',     'properties', 'fo:font-style', 'italic', 0, '\I<', '>'],
            ['U',     'properties', 'style:text-underline', 'single', 0, '\U<', '>'],                # OO 1.0
            ['U',     'properties', 'style:text-underline-style', 'solid', 0, '\U<', '>'],           # OD (missing the previous part -bug?!)
            ['F',     'properties', 'fo:color', qr/^(\#[\da-fA-F]{6})$/, 0, '\F{color="$1"}<', '>'], # first backslash is for highlightning
            ['C',     'properties', 'style:font-name', qr/^(Courier New)$/, 0, '\C<', '>'],
            ['BLOCK', 'references', 'style:parent-style-name', qr/^(Code)$/, 0, ' ' x 3, ''],
           );



# init wrappers
my ($paragraphWrapper, $listWrapper);
$paragraphWrapper=Text::Wrapper->new(
                                     columns	=> 76,
                                     par_start	=> '',
                                     body_start	=> ''
                                    );
  
$listWrapper=Text::Wrapper->new(
                                columns		=> 76,
                                par_start	=> '  ',
                                body_start	=> '  '
                               );

=pod

=head2 new()

The constructor.

B<Parameters:>

All parameters except the first are named.

=over 4

=item class

The target class name. This parameter is set automatically if you use the usual Perl syntax
to call a constructor.

=item file

The (absolute or relative) path to the Office document that should be converted.

=item imagebufferdir

OO document images refer to images stored within the document or located externally at
a location that is specified by an URL. Both image sources cannot be accessed by PerlPoint,
so the converter makes copies from those sources and refers to I<them>. The C<imagebufferdir>
option specifies where these intermediate copies should be stored. The directory is made
unless it already exists.

A I<relative> path will result in a directory relative to the document. An I<absolute> path
is suitable if images from various documents should be collected in one place, or if the
resulting PerlPoint document should be written to a special path.


=item metadataTemplate

A template to include document meta data to the transformation result. The template is
expected to be in C<Text::Template> format, in a safe compartment.

These data are available:

=over 4

=item %metadata

A hash of all document meta data. The keys of this hash are the following, while the
values hold the document data assigned to that keys.

=over 4
    
=item title

document title

=item subject

document subject

=item description

document description

=item creator

document author

=item date

last modification

=item keywords

keywords describing the document

=item User defined fields

All names defined by C<userdefinedDocdata>.

=back

=item %tools

Keys: C<generator> holds the name of the program that wrote the OO document.
C<converter> holds the name of the converter, usually the name of this module.

=item $source

Source name, usually the name set by option C<file>.

=back

This option has no effect if C<skipmetadata> is set.


=item skipmetadata

If set to a true value meta data processing is bypassed.


=item userdefinedDocdata

Each OO document can be described by various predefined data, which are set automatically
(like the modification date) or set up by the document author in a dialog (like the
documents title). Additionally, OO allows to define up to four user informations. Called
C<info1> to I<info4> by default, they can be named individually if required.

This option expects a reference to an array of names for those user defined document data entries.
The names can be used in templates passed in via option C<metadataTemplate> to access the data
stored in the related document fields.

=back

B<Returns:> the new object.

B<Example:>

 # build an object
 my $oo2pp=new OpenOffice::PerlPoint(file=>$ooFile);

=cut
sub new
 {
  # get parameters
  my ($class, @pars)=@_;

  # build parameter hash
  confess "[BUG] The number of parameters should be even - use named parameters, please.\n" if @pars%2;
  my %pars=@pars;

  # check parameters
  confess "[BUG] Missing class name.\n" unless $class;
  confess "[BUG] Missing file parameter.\n" unless exists $pars{file};
  confess "[BUG] Missing image buffer directory parameter.\n" unless exists $pars{imagebufferdir};

  # build object
  my __PACKAGE__ $me=fields::new($class);

  # store configuration
  $me->{$_}=$pars{$_} for qw(
                             file
                             imagebufferdir
                             skipmetadata
                             metadataTemplate
                             userdefinedDocdata
                            );

  # aggregate a user agent object
  $me->{userAgent}=new LWP::UserAgent;
  $me->{userAgent}->timeout(1);
  $me->{userAgent}->env_proxy;

  # and a Net::Ping object
  $me->{ping}=new Net::Ping;

  # build archive object
  $me->{archive}=ooFile($pars{file});
  confess "[Error] $pars{file} is no regular OpenOffice.org file.\n" unless $me->{archive};

  # extract metadata
  $me->{metadata}=ooMeta(archive => $me->{archive});
  carp "[Warn] $pars{file} has not standard OOO properties, it looks strange.\n" unless $me->{metadata};

  # extract document (in content and style parts)
  $me->{docContent}=ooDocument(
                               archive    => $me->{archive},
                               member     => 'content',
                               delimiters => \%delimiters,
                              );
  confess "[Error] No standard OOO content found in $pars{file}!\n" unless $me->{docContent};

  $me->{docStyles}=ooDocument(
                              archive    => $me->{archive},
                              member     => 'styles',
                              delimiters => \%delimiters,
                             );
  confess "[Error] No standard OOO styles found in $pars{file}!\n" unless $me->{docContent};

  # the strange next lines prevent the getText() method of
  # OpenOffice::OODoc::Text (see the corresponding man page) from using
  # its default tags for spans and footnotes
  delete $me->{docContent}{delimiters}{'text:span'};
  delete $me->{docContent}{delimiters}{'text:footnote-body'};

  # here we select the tab as field separator for table field output
  # (the default is ";" as for CSV output)
  $me->{docContent}{field_separator}="\t";

  # in the next sequence, we will extract all the footnotes, store them for
  # later processing and remove them from the content
  $me->{notes}=[$me->{docContent}->getFootnoteList];
  $me->{docContent}->removeElement($_) for @{$me->{notes}};

  # get the full list of text objects (without the previously removed footnotes)
  $me->{content}=[$me->{docContent}->getTextElementList];

  # reply the new object
  $me;
 }

# TODO: make document variable names configurable
sub convertMetadata
 {
  # get and check parameters
  ((my __PACKAGE__ $me), (my ($item, $guard)))=@_;
  confess "[BUG] Missing object parameter.\n" unless $me;
  confess "[BUG] Object parameter is no ", __PACKAGE__, " object.\n" unless ref $me and ref $me eq __PACKAGE__;

  # variables
  my ($perlpoint, $title, $subject, $description, $author, $date, $version, $generator, $copyright, $authormail);

  # anything to do?
  if ($me->{metadata})
    {
     # predefined meta data: title, subject, description, author
     $me->{metaData}{$_}=$me->{metadata}->$_ || 'unknown' for qw(
                                                                 creator
                                                                 date
                                                                 description
                                                                 keywords
                                                                 subject
                                                                 title
                                                                );

     # get user defined metadata, as set up by caller
     my %userDefinedMetadata=$me->{metadata}->user_defined;
     $me->{metaData}{$_}=$userDefinedMetadata{$_} || 'unknown' for @{$me->{userdefinedDocdata}};

     # get generator
     $generator=$me->{metadata}->generator;
     $generator='unknown program' unless $generator;
    }

  # process meta data, if configured
  if (defined $me->{metadataTemplate})
   {
    # build safe environment
    my $safe=new Safe;

    # clone meta data into a transfer variable
    my %transfer=(
                  # meta data
                  metaData => $me->{metaData} ? Storable::dclone($me->{metaData}) : {},

                  # generator and converter
                  tools    => {
                               generator => $generator,
                               converter => __PACKAGE__,
                              },

                  # more data
                  source   => $me->{file},
                 );

    # build a template object, process the template and add the result
    # (template bugs stop the program immediately - as they are considered bugs, not (user) errors)
    my $template=new Text::Template(TYPE => 'STRING', SOURCE => $me->{metadataTemplate})
        or die "[BUG] Couldn't construct template: $Text::Template::ERROR\n";
    $perlpoint.=$template->fill_in(SAFE => $safe, HASH => \%transfer)
        or die "[BUG] Couldn't process template: $Text::Template::ERROR\n";
   }

  # supply result
  $perlpoint;
 }


#-----------------------------------------------------------------------------

# convert completely
=pod

=head2 oo2pp()

Perform conversion of the document specified in the constructor call.

B<Parameters:>

=over 4

=item object

A object as supplied by C<new()>.

=back

B<Returns:> the PerlPoint string.

B<Example:>

 # convert document
 my $perlpoint=$oo2pp->oo2pp;

=cut
sub oo2pp
 {
  # get and check parameters
  ((my __PACKAGE__ $me), (my ($item, $guard)))=@_;
  confess "[BUG] Missing object parameter.\n" unless $me;
  confess "[BUG] Object parameter is no ", __PACKAGE__, " object.\n" unless ref $me and ref $me eq __PACKAGE__;

  # variables
  my ($perlpoint);

  # meta data, unless suppressed
  $perlpoint.=$me->convertMetadata unless $me->{skipmetadata};

  # content
  $perlpoint.=$me->convertContent;

  # supply result
  $perlpoint;
 }

#-----------------------------------------------------------------------------

# build a headline
sub buildHeadline
 {
  # get and check parameters
  my ($me, $element)=@_;
  confess "[BUG] Missing object parameter.\n" unless $me;
  confess "[BUG] Object parameter is no ", __PACKAGE__, " object.\n" unless ref $me and ref $me eq __PACKAGE__;
  confess "[BUG] Missing element parameter.\n" unless defined $element;
  confess "[BUG] Element parameter is no ", 'XML::Twig::Elt', " object.\n" unless ref $element and $element->isa('XML::Twig::Elt');

  # build headline and supply result
  $me->constructHeadline($me->{docContent}->getLevel($element), $me->{docContent}->getText($element) || 'EMPTY HEADLINE FOUND - CHECK SOURCE DOCUMENT FORMATTING, PLEASE ~ EMPTY HEADLINE');
 }

#-----------------------------------------------------------------------------

# a low level routine to build a headline
sub constructHeadline
 {
  # get and check parameters
  ((my __PACKAGE__ $me), (my ($level, $text, $short)))=@_;
  confess "[BUG] Missing object parameter.\n" unless $me;
  confess "[BUG] Object parameter is no ", __PACKAGE__, " object.\n" unless ref $me and ref $me eq __PACKAGE__;
  confess "[BUG] Missing level parameter.\n" unless defined $level;
  confess "[BUG] Missing text parameter.\n" unless $text;

  # build headline and supply result
  join('', '=' x $level, $text, (defined $short ? " ~ $short" : ()), "\n\n");
 }

#-----------------------------------------------------------------------------

# a low level routine to build a comment
sub constructComment
 {
  # get and check parameters
  ((my __PACKAGE__ $me), (my ($text)))=@_;
  confess "[BUG] Missing object parameter.\n" unless $me;
  confess "[BUG] Object parameter is no ", __PACKAGE__, " object.\n" unless ref $me and ref $me eq __PACKAGE__;
  confess "[BUG] Missing text parameter.\n" unless $text;

  # build comment and supply result
  join('', '// ', $text, "\n");
 }

#-----------------------------------------------------------------------------

# build a separate note section (at the end of the document)
sub	buildNoteBlock
 {
  # get and check parameters
  my ($me)=@_;
  confess "[BUG] Missing object parameter.\n" unless $me;
  confess "[BUG] Object parameter is no ", __PACKAGE__, " object.\n" unless ref $me and ref $me eq __PACKAGE__;

 }

#-----------------------------------------------------------------------------

# build content from element
sub buildContent
 {
  # get and check parameters
  my ($me, $element)=@_;
  confess "[BUG] Missing object parameter.\n" unless $me;
  confess "[BUG] Object parameter is no ", __PACKAGE__, " object.\n" unless ref $me and ref $me eq __PACKAGE__;
  confess "[BUG] Missing element parameter.\n" unless defined $element;
  confess "[BUG] Element parameter is no ", 'XML::Twig::Elt', " object.\n" unless ref $element and $element->isa('XML::Twig::Elt');

  # variables
  my ($perlpoint)=('');

  # get element text
  my $text=$me->{docContent}->getText($element);

  # choose an output format according to the type
  if ($element->isItemList)
   {
    # try to find out whether this list is ordered or not
    my $prefix=$element->isOrderedList ? '#' : '*';

    # handle all list elements
    foreach my $item ($me->{docContent}->getItemElementList($element))
     {
      # transform text
      my $transformed=$listWrapper->wrap($me->traverseElement($item, \&guardSpecials));

      # write it, if necessary
      $perlpoint.=$transformed ? ("$prefix $transformed\n") : ();
     }
   }
  elsif ($element->isTable)
   {
    # a table: get a table handle, table dimensions and the table text
    my $table=$me->{docContent}->getTable($element);
    my ($rowNr, $colNr)=$me->{docContent}->getTableSize($table);
    my $tableText=$me->{docContent}->getTableText($table);

    # set column separator
    my $columnSeparator='|';
    {
     my ($i, $cs)=(1, $columnSeparator);
     ++$i, $cs=quotemeta($columnSeparator='|' x $i) while $tableText=~/$cs/g;
    }
      
    # start table (TODO: switch to nested tables)
    $perlpoint.="\@$columnSeparator\n";

    # handle all rows
    foreach my $row ($me->{docContent}->getTableRows($table))
     {
      # cell value collector
      my (@cellValues);

      # handle all cells
      foreach my $cellNr (0..$colNr-1)
       {
        # get cell handle
        my $cell=$me->{docContent}->getCell($row, $cellNr);

        # get content
        push(@cellValues, $me->{docContent}->getCellValue($cell));
       }

      # add row
      $perlpoint=join('', $perlpoint, join(" $columnSeparator ", map {(defined) ? $_ : ''} @cellValues), "\n");
     }

    # complete table
    $perlpoint.="\n";
   }
  else
   {
    # scopies
    my ($empty, $prefix, $suffix)=('');

    # get paragraph style and its attributes
    my $styleName=$me->{docContent}->getStyle($element);
    my $styleObject=$me->{docContent}->getStyleElement($styleName) || $me->{docStyles}->getStyleElement($styleName);
    my %attributes=$me->{docContent}->getStyleAttributes($styleObject);

#    use Data::Dumper; warn Dumper \%attributes;

    # set paragraph prefix
    if (
            exists $attributes{references}{'style:family'}
        and $attributes{references}{'style:family'} eq 'paragraph'
        and exists $attributes{references}{'style:parent-style-name'}
        and $attributes{references}{'style:parent-style-name'}=~/^(Code)$/
       )
     {
      # a code block
      $prefix=' ' x 3;
      $suffix='';
      $empty="\n";
     }
    else
     {
      # default to a text paragraph
      $prefix='.';
      $suffix="\n";
     }

    # transform text
    my $transformed=$paragraphWrapper->wrap($me->traverseElement($element, \&guardSpecials));

    # write it, if necessary
    $perlpoint.=$transformed ? ("$prefix$transformed$suffix") : $empty;
   }

  # supply result
  $perlpoint;
 }

#-----------------------------------------------------------------------------

# convert content
sub convertContent
 {
  # get and check parameters
  my ($me)=@_;
  confess "[BUG] Missing object parameter.\n" unless $me;
  confess "[BUG] Object parameter is no ", __PACKAGE__, " object.\n" unless ref $me and ref $me eq __PACKAGE__;

  # variables
  my ($perlpoint)=("\n\n");

  # handle all elements
  foreach my $element (@{$me->{content}})
   {
    # get element level
	if ($element->isHeader)
     {$perlpoint.=$me->buildHeadline($element);}
	else
     {$perlpoint.=$me->buildContent($element);}
   }

  # supply result
  $perlpoint;
 }

#-----------------------------------------------------------------------------


# Traverse an element to find style based formattings and produce appropriate markup.
# This is a proof of concept and needs cleanup (constants for indices etc.).
# The base traversal idea was taken from getText().
sub traverseElement
 {
  # get and check parameters
  ((my __PACKAGE__ $me), (my ($item, $guard)))=@_;
  confess "[BUG] Missing object parameter.\n" unless $me;
  confess "[BUG] Object parameter is no ", __PACKAGE__, " object.\n" unless ref $me and ref $me eq __PACKAGE__;
  confess "[BUG] Guard parameter should be a code reference.\n" if $guard and ref($guard) ne 'CODE';

  # declare vars
  my ($result)=('');


  # node?
  if ($item->isElementNode)
   {
    # scopy
    my @matches;

    # get item name
    my $itemName=$item->getName();


    # special cases first: whitespace
    if ($itemName eq 'text:s')
     {
      # this stands for a number of whitespaces (TODO: take care of text:c)
      return ' ';
     }
    elsif ($itemName eq 'text:line-break')
     {
      # a tab stop character (TODO: could be translated into a whitespace sequence)
      return ' ';
     }
    elsif ($itemName eq 'text:tab-stop')
     {
      # a tab stop character (TODO: could be translated into a whitespace sequence)
      return "\t";
     }
    elsif ($itemName=~/^text:table-of-content$/)
     {
      # ignore TOC's (TODO: make this configurable)
      return '';
     }

    # get paragraph style and its attributes
    my $styleName=$me->{docContent}->getStyle($item);
    my $styleObject=$me->{docContent}->getStyleElement($styleName) || $me->{docStyles}->getStyleElement($styleName);
    my %attributes=(defined $me->{docContent}->getStyle($item)) ? $me->{docContent}->getStyleAttributes($styleObject) : ();

#    use Data::Dumper; warn Dumper \%attributes;

    # special case: image
    if ($item->isImage)
     {
      # get image element
      $item=$me->{docContent}->getImageElement($item);

      # extract image data
      my (
          $imageName,
          $imageLink,
          $imageDescription
         )=(
            $me->{docContent}->imageName($item),
            $me->{docContent}->imageLink($item),
           # $me->{docContent}->imageDescription($item),
           );
                                                      

      # make a buffer directory, if necessary (TODO: make path configurable)
      mkdir($me->{imagebufferdir}) unless -d $me->{imagebufferdir};

      # export internal graphics
      if ($imageLink=~m{^(\#)?Pictures/})
       {
        # export image and adapt source link
        $me->{docContent}->exportImage($item, $imageLink=join('/', $me->{imagebufferdir}, basename($imageLink)));
       }

      # import external graphics
      elsif ($imageLink=~m{^https?://([^/]+)})
       {
        # try to get the image file
        warn "[Info] Trying to fetch $imageLink from $1.\n";

        # buffer host name
        my ($imageHost)=$1;
        
        # host reachable?
        if ($me->{ping}->ping($imageHost, 1))
         {
          my $file=$me->{userAgent}->get($imageLink);
          warn "[Info] Fetched (success: ", $file->is_success, ").\n";

          # success?
          if ($file->is_success)
           {
            # try to store the file locally
            if (open(my $copy, join('', '>', $imageLink=join('/', $me->{imagebufferdir}, basename($imageLink)))))
             {print $copy $file->content;}
            else
             {return qq(.\\I<CONVERTER NOTE:> Image $imageName not importable: could not open local buffer file $imageLink ($!).);}
           }
          else
           {return qq(.\\I<CONVERTER NOTE:> Image $imageName not importable from $imageHost.);}
         }
        else
         {return qq(.\\I<CONVERTER NOTE:> Image host $imageHost not reachable to get $imageName.);}
      }

      # build a tag and return it (TODDO: generalize)
      return qq(\\IMAGE{alt="$imageName" src="$imageLink"});
     }

    # we need buffers
    my ($prefixPart, $contentPart, $suffixPart);

    # open new tags
    foreach my $style (@styles)
     {
      # translate formatting into tags if possible
      if (
              exists $attributes{$style->[1]}{$style->[2]}
          and (
                  # string match?
                  (not ref($style->[3]) and $attributes{$style->[1]}{$style->[2]} eq $style->[3])
               # pattern match?
               or (ref($style->[3]) and (@matches=$attributes{$style->[1]}{$style->[2]}=~/$style->[3]/))
              )
         )
        {
         # anything to do?
         next unless ++$style->[4]==1;

         # get prefix string
         my $prefix=$style->[5];

         # replace placeholders, if necessary
         if (ref($style->[3]))
          {
           # replace last match results
           $prefix=~s/\$(\d+)/$matches[$1-1]/g;
          }

         # add prefix
         $prefixPart=join('', defined $prefixPart ? $prefixPart : '', $prefix);
        }
     }

    # recursive call
    $contentPart.=$me->traverseElement($_, $guard) foreach $item->getChildNodes;

    # close tags that were opened on this level
    foreach my $style (reverse @styles)
     {
      # translate formatting into tags if possible
      if (
              exists $attributes{$style->[1]}{$style->[2]}
          and (
                  # string match?
                  (not ref($style->[3]) and $attributes{$style->[1]}{$style->[2]} eq $style->[3])
               # pattern match?
               or (ref($style->[3]) and $attributes{$style->[1]}{$style->[2]}=~/$style->[3]/)
              )
         )
        {
         # anything to do?
         next if --$style->[4];

         # get suffix string
         my $suffix=$style->[6];

         # replace placeholders, if necessary
         if (ref($style->[3]))
          {
           # save last match results
           $suffix=~s/\$(\d+)/$matches[$1-1]/g;
          }

         # add prefix
         $suffixPart=join('', defined $suffixPart ? $suffixPart : '', $suffix);
        }
     }

    # now combine the parts as necessary
    $result.=join('',
                  defined $prefixPart ? $prefixPart : '',
                  $contentPart,
                  defined $suffixPart ? $suffixPart : '',
                 ) if defined $contentPart and $contentPart;
   }
  else
   {
    # get text, guard specials, and add the result to the functions result string
    my $text=$me->{docContent}->outputTextConversion($item->getValue() || '');
    $text=$guard->($text) if $text and $guard;
    $result.=$text;
   }

  # supply result
  $result;
 }


# class method: a translator for characters that are special in the target language
sub guardSpecials
 {
  # get and check parameters
  my ($text)=@_;

  # translate
  $text=~s/([\$>\\])/\\$1/g;

  # supply modified text
  $text;
 }



# flag successfull load
1;




# = POD TRAILER SECTION =================================================================

=pod

=head1 NOTES

First of all, please note the I<alpha state> of this software. C<OpenOffice::PerlPoint>
does support just a subset of the very rich features potentially occuring in an Open Office document.
Some features should be added later, other features have no expression in PerlPoint
and therefore are ignored.

=head2 Supported features

=over 4

=item Meta data

Selected meta data are transformed into PerlPoint variables. The names of these variables
are fix in the current version of C<OpenOffice::PerlPoint>, but shall become configurable
in later versions.

=over 4

=item Title

Stored in C<$docTitle.>

=item Subject

Stored in C<$docSubtitle.>

=item Description

Stored in C<$docDescription.>

=item Author

Stored in C<$docAuthor.>

=item Date

Stored in C<$docDate.>

=item Version

If the user defines a data named C<version>, it is stored in C<$docVersion>.

=item Copyright

If the user defines a data named C<copyright>, it is stored in C<$docCopyright>.

=back


=item Headlines

Headlines are supported. Make sure to use the predefined headline formats in the Office document,
and avoid gaps in headline hierarchies (as they will cause PerlPoint translation errors later).


=item Text formatting

Bold, italic, underlined, colorized text portions within a paragraph are translated into
the related PerlPoint tags C<\B>, C<\I>, C<\U> and C<\F>.

=item Text marked as code

In Perlpoint, text within a paragraph can be marked as "code" by the C<\C> tag. As there
is no comparable feature in OpenOffice (I know of), all text assigned to the font I<Courier New>
is treated as such code.

The font is fix for now, but shall be configurable in a future version.

=item Blocks

In PerlPoint, examples can be written into "code blocks", which are paragraphs marked by
indentation. Open Office as an office suite is not focussed on code, so again there is a
convention. All paragraphs assigned to a style "Code" are treated as blocks.

The style name is fixed for now, but shall be configurable in a future version.


=item Lists

Lists are basically supported.

Unfortunately, it is difficult to distinguish between ordered and bullet lists in OASIS OpenDocument.
That's why ordered lists are transformed into bullet lists if an OpenDocument source is translated.
For OpenOffice 1.0 documents ordered lists are handled correctly.


=item Tables

Tables are supported as long as they are not nested.


=item Images

Images I<embedded into the document> are fully supported.


=item Comments

Comments are supported.


=back


=head2 Limitations

=over 4

=item Limited to text documents

The current version can handle text documents I<only>. Spreadsheets, presentations etc. cannot
be transformed at this time.


=item Footnote support

... is invalid yet. Current results will not pass a PerlPoint converter.


=item OASIS OpenDocument

is not fully supported at the moment due to the beta status of both Open Office 2.0 and
C<OpenOffice::OODoc>. As both tools are well supported this is just a matter of time.

=back


=head2 TOCs

Office document tables of contents cannot easily be transformed into PerlPoint TOCs. That's why
they are ignored.


=head2 POD

It should be possible to adapt this library for POD output. This might be done in the future.
Both versions could use a common base library.


=head1 Credits

This module is based on C<OpenOffice::OODoc>. Thanks to its author
Jean-Marie Gouarné for the module and his helpful support with many questions.


=head1 TODO

=over 4

=item *

Support nested tables.

=item *

TOC ignoration could become configurable.

=item *

Optionally image file copies stored in a buffer directory should be named generically.

=item *

Support other formats like spreadsheets and presentations.

=back


=head1 SEE ALSO

=over 4

=item B<OpenOffice::OODoc>

The module that made it possible to write C<OpenOffice::PerlPoint> relatively quickly.

=item B<Bundle::PerlPoint>

A bundle of packages to deal with PerlPoint documents.

=item B<oo2pp>

A OpenOffice / OpenDocument format to PerlPoint translator, distributed and installed with this module.


=back


=head1 SUPPORT

A PerlPoint mailing list is set up to discuss usage, ideas,
bugs, suggestions and translator development. To subscribe,
please send an empty message to perlpoint-subscribe@perl.org.

If you prefer, you can contact me via perl@jochen-stenzel.de
as well.


=head1 AUTHOR

Copyright (c) Jochen Stenzel (perl@jochen-stenzel.de), 2005.
All rights reserved.

Parts of the module are derived from an C<oo2pod> example script
that came with C<OpenOffice::OODoc> 1.309.

This module is free software, you can redistribute it and/or modify it
under the terms of the Artistic License distributed with Perl version
5.003 or (at your option) any later version. Please refer to the
Artistic License that came with your Perl distribution for more
details.

The Artistic License should have been included in your distribution of
Perl. It resides in the file named "Artistic" at the top-level of the
Perl source tree (where Perl was downloaded/unpacked - ask your
system administrator if you dont know where this is).  Alternatively,
the current version of the Artistic License distributed with Perl can
be viewed on-line on the World-Wide Web (WWW) from the following URL:
http://www.perl.com/perl/misc/Artistic.html


=head1 DISCLAIMER

This software is distributed in the hope that it will be useful, but
is provided "AS IS" WITHOUT WARRANTY OF ANY KIND, either expressed or
implied, INCLUDING, without limitation, the implied warranties of
MERCHANTABILITY and FITNESS FOR A PARTICULAR PURPOSE.

The ENTIRE RISK as to the quality and performance of the software
IS WITH YOU (the holder of the software).  Should the software prove
defective, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.

IN NO EVENT WILL ANY COPYRIGHT HOLDER OR ANY OTHER PARTY WHO MAY CREATE,
MODIFY, OR DISTRIBUTE THE SOFTWARE BE LIABLE OR RESPONSIBLE TO YOU OR TO
ANY OTHER ENTITY FOR ANY KIND OF DAMAGES (no matter how awful - not even
if they arise from known or unknown flaws in the software).

Please refer to the Artistic License that came with your Perl
distribution for more details.