The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package Syndication::ESF;

=head1 NAME

Syndication::ESF - Create and update ESF files

=head1 SYNOPSIS

    use Syndication::ESF;

    my $esf = Syndication::ESF->new;

    $esf->parsefile( 'my.esf' );

    $esf->channel( title => 'My channel' );

    $esf->add_item(
        date  => time,
        title => 'new item',
        link  => 'http://example.org/#foo'
    );

    print "Channel: ", $esf->channel( 'title' ), "\n";
    print "Items  : ", scalar @{ $esf->{ items } }, "\n";

    my $output = $esf->as_string;

    $esf->save( 'my.esf' );

=head1 DESCRIPTION

This module is the basic framework for creating and maintaing Epistula Syndication
Format (ESF) files. More information on the format can be found at the Aquarionics
web site: http://www.aquarionics.com/article/name/esf

This module tries to copy the XML::RSS module's interface. All applicable methods
have been copied and should respond in the same manner.

Like in XML::RSS, channel data is accessed through the C<channel()> sub, and item
data is accessed straight out of the items array.

=head1 INSTALLATION

    perl Makefile.PL
    make
    make test
    make install

=cut

use strict;
use warnings;
use Carp;

our $VERSION = '0.13';

# Defines the set of valid fields for a channel and its items
my @channel_fields = qw( title contact link );
my @item_fields    = qw( date title link );

=head1 METHODS

=head2 new()

Creates a new Syndication::ESF object. It currently does not accept any parameters.

=cut

sub new {
    my $class = shift;
    my $self  = {
        channel => {},
        items   => []
    };

    bless $self, $class;

    return $self;
}

=head2 channel(title => $title, contact => $contact, link => $link)

Supplying no parameters will give you a reference to the channel data. Specifying
a field name returns the value of the field. Giving it a hash will update the channel
data with the supplied values.

=cut

sub channel {
    my $self = shift;

    # accessor; if there's only one arg
    if ( @_ == 1 ) {
        return $self->{ channel }->{ $_[ 0 ] };
    }

    # mutator; if there's more than one arg
    elsif ( @_ > 1 ) {
        my %options = @_;

        for ( keys %options ) {
            $self->{ channel }->{ $_ } = $options{ $_ };

            # extract email and name from contact info
            if ( $_ eq 'contact' ) {
                my @contact = split( / /, $options{ $_ }, 2 );
                $contact[ 1 ] =~ s/[\(\)]//g;
                $self->channel(
                    'contact_name'  => $contact[ 1 ],
                    'contact_email' => $contact[ 0 ]
                );
            }
        }
    }

    return $self->{ channel };
}

=head2 contact_name()

shortcut to get the contact name

=cut

sub contact_name {
    my $self = shift;
    return $self->channel( 'contact_name' );
}

=head2 contact_email()

shortcut to get the contact email

=cut

sub contact_email {
    my $self = shift;
    return $self->channel( 'contact_email' );
}

=head2 add_item(date => $date, title => $title, link => $link, mode => $mode)

By default, this will append the new item to the end of the list. Specifying
C<'insert'> for the C<mode> parameter adds it to the front of the list.

=cut

sub add_item {
    my $self    = shift;
    my $options = { @_ };
    my $mode    = $options->{ mode };

    # depending on the mode, add the item to the
    # start or end of the feed
    if ( $mode and $mode eq 'insert' ) {
        unshift( @{ $self->{ items } }, $options );
    }
    else {
        push( @{ $self->{ items } }, $options );
    }

    return $self->{ items };
}

=head2 parse($string)

Parse the supplied raw ESF data.

=cut

sub parse {
    my $self = shift;
    my $data = shift;

    # boolean to indicate if we're parsing the meta data or the items.
    my $metamode = 1;

    foreach my $line ( split /(?:\015\012|\012|\015)/, $data ) {

        # skip to the next line if it's a comment
        next if $line =~ /^#/;

        chomp( $line );

        # if it's a blank line, get out of meta-mode.
        if ( $line eq '' ) {
            $metamode = 0;
            next;
        }

        my @data = split /\t/, $line;

        # depending on what mode we're in, insert the channel, or item data.
        if ( $metamode ) {
            $self->channel( $data[ 0 ] => $data[ 1 ] );
        }
        else {
            push @{ $self->{ items } },
                { map { $item_fields[ $_ ] => $data[ $_ ] }
                    0 .. $#item_fields };
        }
    }
}

=head2 parsefile($filename)

Same as C<parse()>, but takes a filename as input.

=cut

sub parsefile {
    my $self = shift;
    my $file = shift;

    open( my $esf, $file ) or croak "File open error ($file): $!";

    my $data = do { local $/; <$esf>; };

    close( $esf ) or carp( "File close error ($file): $!" );

    $self->parse( $data );
}

=head2 as_string()

Returns the current data stored in the object as a string.

=cut

sub as_string {
    my $self = shift;

    my $data;

    # append channel data
    $data .= "$_\t" . $self->channel( $_ ) . "\n" for @channel_fields;
    $data .= "\n";

    # append item data
    foreach my $item ( @{ $self->{ items } } ) {
        $data .= $item->{ $_ } . "\t" for @item_fields;
        $data =~ s/\t$/\n/;
    }

    return $data;
}

=head2 save($filename)

Saves the value of C<as_string()> to the supplied filename.

=cut

sub save {
    my $self = shift;
    my $file = shift;

    open( my $esf, ">$file" ) or croak "File open error ($file): $!";

    print { $esf } $self->as_string;

    close( $esf ) or carp( "File close error ($file): $!" );
}

=head1 AUTHOR

Brian Cassidy E<lt>bricas@cpan.orgE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright 2003-2009 by Brian Cassidy

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself. 

=head1 SEE ALSO

=over 4

=item * L<XML::RSS>

=back

=cut

1;