package Text::DDF;

require Exporter;
require 5.003;

$VERSION = 0.01;
@ISA = Exporter;
@EXPORT = qw(&ddfencode);
@EXPORT_OK = qw($strict_7bit);

$strict_7bit = 0;

sub ddfencode {
    foreach (@_)
    {
	s/\{/\{\\123\}/g;
    }
    if( $strict_7bit )
    {
	foreach (@_)
	{
	    s/[\200-\377]/'{\\' . ord ($&) . '}'/ge;
	}
    }
    @_;
}

=head1 NAME

Text::DDF -- package for creating Impression Document Description Format

=head1 SYNOPSIS

	use Text::DDF
	
	print ddfencode(@text);


	use Text::DDF qw(ddfencode $strict_7bit);
	
	$strict_7bit = 1;
	ddfencode(@text);

	$index = new Text::DDF::Style 'Index', qw (index on);
	print $index->StyleWrap( @text );

=head1 DESCRIPTION

B<Text::DDF> provides subroutines and classes for creating files in Impression
B<D>ocument B<D>escription B<F>ormat.

C<Text::DDF::ddfencode> converts the array to C<DDF>, converting all
instances of 'C<{>' to 'C<{\123}>'. If C<Text::DDF::strict_7bit> is true,
it will also convert top-bit-set characters to 'C<{\###}>' notation.

C<Text::DDF::ddfencode> modifies the array passed, and for ease of use
additionally returns the array that it converted.

=head2 Effects and Styles

C<Text::DDF> provides classes for manipulating effects and styles -
C<Text::DDF::Effect>, C<Text::DDF::Justify>, C<Text::DDF::Style>. The overloaded
interface hides the differences in the DDF syntax used to turn styles and
effects on and off.

=head2 Common methods

=over 4

=item new <name> ... 

Declare a style/effect with this name.

	$bold = new Text::DDF::Effect 'bold';
	$centre = new Text::DDF::Justify 'centre';
	$normal = new Text::DDF::Style 'Normal', qw(
	  font Trinity.Medium
	  fontsize 12pt );

Arguments after the style name are passed to the C<Add> method

	$no_op = new Text::DDF::Effect;

defines an empty effect

=item StyleOn

=item StyleOff

Return the C<DDF> instruction to turn the style/effect on/off.

=item StyleWrap

Return the C<DDF> turn the style/effect on, the verbatim text, the DDF to
turn the style/effect off. In array context returns an array:

	(
	 StyleOn(),
	 @_,
	 StyleOf()
	)

In scalar context this array is concatenated

=item Define()

Returns the C<DDF> needed to defined the style/effect. Returns an empty string
for an effect, so that a mixed array of effects and styles can be C<Define>d
safely.

=item Copy <newname>

Returns a new style/effect clone. (with the same class as its parent.)

=item Print

Returns a formatted definition of the style/effect. This is mostly of use to the
C<Define> method.

In array context effects return an empty array, whereas styles return an array
of lines.

In scalar context styles return text of the form:

	style  "Normal";
	 font Trinity.Medium;
	 fontsize 12pt;

effects return:
	effect bold;

B<NB> scalars are terminated by "C<\n>"

=back

=head2 Style Methods

=over 4

=item Add

Takes pairs of C<attribute, value> and adds them to the style definition.

	$verbatim->Add( qw(spacebelow 0pt) );

You can use C<AddTabs> and C<AddTabComma> to add tabs, but the C<Add> method
will spot lines of the form

	Add( 'tabs', 'r18pt 24pt' );

and call C<AddTabsComma>.

=item AddTabs

Takes an array of tabs, and adds them to the style's array of tabs.

Tabs are strings such as

	r18pt
	25.4mm
	c4pi

The available prefixes are

=over 2

=item l

Left justify tab (the default if no prefix)

=item c

Centre justify tab

=item r

Right justify tab

=item d

Decimal tab

=item v

Vertical rule off

=back

	$itemlist->AddTabs( qw( r18pt 24pt 72pt 144pt 216pt 288pt 360pt 432pt 504pt ) );

=item AddTabsComma

Adds a comma separated list of tabs. Literally:

    sub AddTabsComma {
	my $self = shift;
	$self->AddTabs( split (/,/, $_[0]) );
    }

=item Get <attribute> ...

Returns an array of values corresponding to the array of attributes
passed

=item Delete <attribute> ...

Delete values corresponding to the array of attributes passed

=item GetTabs

In array context returns the sorted array of tabs in the format

	right 18pt
	centre 3pi
	 25.5mm

In scalar context joins the list with commas

	right 18pt,centre 3pi, 25.5mm

=item DeleteTabs

Not implemented yet.

=item PrintTabs

In array context returns the sorted array of tabs in the format

	right 18pt
	centre 3pi
	 25.5mm

In scalar context joins the list with commas and prefixes 'tabs'

	tabs right 18pt,centre 3pi, 25.5mm
	
or returns '' if there are no tabs defined.

=back

=head1 BUGS

=head2 Shortcomings of Impression's DDF interpreter

=over 4

=item *

DDF effects don't nest, whereas styles do.

	{bold on}Hello {bold on}W{bold}orld!{bold}

	{"bold" on}Hello {"bold" on}W{"bold"}orld!{"bold"}

produce

B<Hello W>orld!

B<Hello World!>

respectively if C<"bold"> is a style with the C<bold> attribute.

=item *

Even in Impression 4.11 the DDF exporter can't cope with style names
containing C<"> characters. Text::DDF::Style makes no checks about
the validity of characters in the style name.

=item *

There's no way to C<{keepregion}> two adjacent regions.

	{keepregion on}Pugh, Pugh, Barney McGrew,
	Cuthbert, Dibble, Grub
	{keepregion}{keepregion on}Time files by when you're the driver of a train,
	and you ride on the footplate there and back again
	{keepregion}

B<won't> allow a breaks between the two regions.

	{keepregion on}Pugh, Pugh, Barney McGrew,
	Cuthbert, Dibble, Grub{keepregion}
	{keepregion on}Time files by when you're the driver of a train,
	and you ride on the footplate there and back again
	{keepregion}

B<will> allow a break between I<Pugh, Pugh, Barney McGrew,> and
I<Cuthbert, Dibble, Grub>.

=back

=head2 Shortcomings of this package

You can't (yet) merge styles.

You can't (yet) merge effects into styles. (This would require a hash/function
to map effect names into style attributes).


=head1 AUTHOR

Nicholas Clark <F<nick@unfortu.net>>

=cut

#	Ruler ISA Effect
#	Style ISA Ruler
# Internal keys start __

################################################################################
package Text::DDF::Effect;

use strict;
use vars qw($VERSION);

$VERSION = 0.01;

# Declare an effect.
sub new {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $self  = {};
    my $name  = shift;
    
    $name = '' unless defined $name;
    
    $self->{'__NAME'} = $name;
    bless ($self, $class);
}

# Turn the effect/style on
#
# DDF Syntax differs
#
# {bold on}	turns on the effect 'bold'
# {"bold" on}	turns on the style "bold" (which isn't the same thing)
#
# But you don't need to know this with an OO Interface. 8-)

sub StyleOn {
    my $self = shift;

    return $self->{'__NAME'} ? "{$self->{'__NAME'} on}" : '';
}

sub StyleOff {
    my $self = shift;

    return $self->{'__NAME'} ? "{$self->{'__NAME'}}" : '';
}


sub StyleWrap {
    my $self = shift;

    if( wantarray )
    {
	return ( $self->StyleOn(), @_, $self->StyleOff() );
    }
    else
    {
	return ( $self->StyleOn() . join( '', @_) . $self->StyleOff() );
    }
}

sub Print {
    return () if( wantarray );
    
    my $self = shift;
    return $self->{'__NAME'} ? "effect $self->{'__NAME'};\n" : '';
}

sub Define {
    '';		# Effects don't need defining.
}

sub Copy {
    my $self = shift;
    my $clone = {%$self};
    bless ($clone, ref($self));
    $clone->{'__NAME'} = $_[0] if defined $_[0];
    $clone;
}

################################################################################
package Text::DDF::Justify;

use strict;
use vars qw($VERSION);

$VERSION = 0.01;

# Declare a justification effect. These differ only in how they are applied

sub StyleOn {
    my $self = shift;

    return "{justify $self->{'__NAME'}}";
}
sub StyleOff {
    return '{justify}';
}

################################################################################
package Text::DDF::Ruler;

use strict;
use vars qw($VERSION @ISA %tabs %special);


@ISA = 'Text::DDF::Effect';
$VERSION = 0.01;

use Carp;
use Units;

%tabs = ( l => '', r => 'right', c => 'centre', d => 'decimal', v => 'vertrule' );

%special = (	'tabs' => [\&AddTabsComma, \&GetTabs, sub {delete shift->{'__TABS'}}] );

sub quote ($) {
    # Will get improved to deal with style names with embedded "
    return "\"$_[0]\"";
}

sub __new ($;) {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $self  = {};
    $self->{'__NAME'} = shift;
    bless ($self, $class);

    $self->Add( @_ );
}

sub new ($;) {
    push @_, 'effectruler', 'on';
    goto &__new
}

sub Name {
    my $self = shift;
    my $oldname = $self->{'__NAME'};
    $self->{'__NAME'} = $_[0] if( defined $_[0] );
    $oldname;
}

sub AddTabs {
    my $self = shift;
    foreach (@_)
    {
	my ($type,$pos) = m/^\s*([rlcdv]?)(.*)/io;
	$type = 'l' unless length $type;
	my $value = convert( 'mm', $pos );
	${$self->{'__TABS'}}{$value} = "$type$pos";
    }
}

sub AddTabsComma {
    my $self = shift;
    $self->AddTabs( split (/,/, $_[0]) );
}

sub Add {
    my $self = shift;
    my ($class, $value);

    while( $class = shift, $value = shift, defined $value )
    {
	if( defined $special{$class} )
	{
	    &{$special{$class}->[0]} ( $self, $value );
	}
	else
	{
	    $self->{$class} = $value;
	}
            
    }

    $self;
}

sub GetTabs {
    my $self = shift;
    my @result;

    foreach (sort { $a <=> $b } keys %{$self->{'__TABS'}})
    {
	my ($type, $pos) = ${$self->{'__TABS'}}{$_} =~ /(.)(.*)/;
	push @result, "$tabs{$type} $pos";
    }

    return @result if wantarray;

    return join( ',', @result );
}

sub Get {
    my $self = shift;
    my (@result, $class);

    while( defined( $class = shift ) )
    {
	if( defined $special{$class} )
	{
	    push @result, &{$special{$class}->[1]} ( $self );
	}
	else
	{
	    push @result, $self->{$class};
	}
    }

    @result;
}

sub Delete {
    my $self = shift;
    my $class;

    while( defined( $class = shift ) )
    {
	if( defined $special{$class} )
	{
	    &{$special{$class}->[2]} ( $self );
	}
	else
	{
	    delete $self->{$class};
	}
    }
}

sub PrintTabs {
    my $self = shift;
    return $self->GetTabs() if wantarray;

    my $answer = scalar $self->GetTabs();
    return length( $answer ) ? "tabs $answer" : '';
}

sub Print {
    my $self = shift;
    my @result = ("style " . quote ($self->{'__NAME'}));
    my $tabs = scalar $self->PrintTabs();
    
    push @result, $tabs unless $tabs eq '';
    
    my ($key, $value);
    while (($key,$value) = each %$self) {
	push @result, "$key $value" unless $key =~ /^__/o;
	}

    return @result if( wantarray );

    return (join ";\n ", @result) . ";\n";
}

sub Define {
    my $self = shift;

    my $result = $self->Print;
    $result =~ s/;\s$//m;
    return "{\ndefine $result}";
}

sub StyleOn {
    my $self = shift;

    return '{' . quote ($self->{'__NAME'}) . ' on}';
}

sub StyleOff {
    my $self = shift;

    return '{' . quote ($self->{'__NAME'}) . ' off}';
}

################################################################################
package Text::DDF::Style;

use strict;
use vars qw($VERSION @ISA);

@ISA = 'Text::DDF::Ruler';
$VERSION = 0.03;


sub new ($;) {
    goto &Text::DDF::Ruler::__new
}

1;
