The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# ABSTRACT: Static website generator

package StaticVolt;
{
  $StaticVolt::VERSION = '0.03';
}

use strict;
use warnings;

use Cwd qw( getcwd );
use File::Copy qw( copy );
use File::Find;
use File::Path qw( mkpath rmtree );
use File::Spec;
use FindBin;
use Template;
use YAML;

use base qw( StaticVolt::Convertor );

use StaticVolt::Convertor::Markdown;
use StaticVolt::Convertor::Textile;

sub new {
    my ( $class, %config ) = @_;

    my %config_defaults = (
        'includes'    => '_includes',
        'layouts'     => '_layouts',
        'source'      => '_source',
        'destination' => '_destination',
    );

    for my $config_key ( keys %config_defaults ) {
        $config{$config_key} = $config{$config_key}
          || $config_defaults{$config_key};
        $config{$config_key} = File::Spec->canonpath( $config{$config_key} );
    }

    return bless \%config, $class;
}

sub _clean_destination {
    my $self = shift;

    my $destination = $self->{'destination'};
    rmtree $destination;

    return;
}

sub _traverse_files {
    my $self = shift;

    push @{ $self->{'files'} }, $File::Find::name;

    return;
}

sub _gather_files {
    my $self = shift;

    my $source = $self->{'source'};
    find sub { _traverse_files $self }, $source;

    return;
}

sub _extract_file_config {
    my ( $self, $fh_source_file ) = @_;

    my $delimiter = qr/^---\n$/;
    if ( <$fh_source_file> =~ $delimiter ) {
        my @yaml_lines;
        while ( my $line = <$fh_source_file> ) {
            if ( $line =~ $delimiter ) {
                last;
            }
            push @yaml_lines, $line;
        }

        return Load join '', @yaml_lines;
    }
}

sub compile {
    my $self = shift;

    $self->_clean_destination;
    $self->_gather_files;

    my $source      = $self->{'source'};
    my $destination = $self->{'destination'};
    for my $source_file ( @{ $self->{'files'} } ) {
        my $destination_file = $source_file;
        $destination_file =~ s/^$source/$destination/;
        if ( -d $source_file ) {
            mkpath $destination_file;
            next;
        }

        open my $fh_source_file, '<', $source_file
          or die "Failed to open $source_file for input: $!";
        my $file_config = $self->_extract_file_config($fh_source_file);

        # For files that do not have a configuration defined, copy them over
        unless ($file_config) {
            copy $source_file, $destination_file;
            next;
        }

        my ($extension) = $source_file =~ m/\.(.+?)$/;

        # If file does not have a registered convertor and is not an HTML file,
        # copy the file over to the destination and skip current loop iteration
        if ( !$self->has_convertor($extension) && $extension ne 'html' ) {
            copy $source_file, $destination_file;
            next;
        }

        # Only files that have a registered convertor need to be handled

        $destination_file =~ s/\..+?$/.html/;    # Change extension to .html

        my $file_layout      = $file_config->{'layout'};
        my $includes         = $self->{'includes'};
        my $layouts          = $self->{'layouts'};
        my $abs_include_path = File::Spec->catfile( getcwd, $includes );
        my $abs_layout_path =
          File::Spec->catfile( getcwd, $layouts, $file_layout );
        my $template = Template->new(
            'INCLUDE_PATH' => $abs_include_path,
            'WRAPPER'      => $abs_layout_path,
            'ABSOLUTE'     => 1,
        );

        my $source_file_content = do { local $/; <$fh_source_file> };
        my $converted_content;
        if ( $extension eq 'html' ) {
            $converted_content = $source_file_content;
        }
        else {
            $converted_content =
              $self->convert( $source_file_content, $extension );
        }

        open my $fh_destination_file, '>', $destination_file
          or die "Failed to open $destination_file for output: $!";
        if ($file_layout) {
            $template->process( \$converted_content, $file_config,
                $fh_destination_file )
              or die $template->error;
        }
        else {
            print $fh_destination_file $converted_content;
        }

        close $fh_source_file;
        close $fh_destination_file;
    }
}

1;



=pod

=head1 NAME

StaticVolt - Static website generator

=head1 VERSION

version 0.03

=head1 SYNOPSIS

    use StaticVolt;

    my $staticvolt = StaticVolt->new;  # Default configuration
    $staticvolt->compile;

=head1 METHODS

=head2 C<new>

Accepts an optional hash with the following parametres:

    # Override configuration (parametres set explicitly)
    my $staticvolt = StaticVolt->new(
        'includes'    => '_includes',
        'layouts'     => '_layouts',
        'source'      => '_source',
        'destination' => '_destination',
    );

=over 4

=item * C<includes>

Specifies the directory in which to search for template files. By default, it
is set to C<_includes>.

=item * C<layouts>

Specifies the directory in which to search for layouts or wrappers. By default,
it is set to C<_layouts>.

=item * C<source>

Specifies the directory in which source files reside. Source files are files
which will be compiled to HTML if they have a registered convertor and a YAML
configuration in the beginning. By default, it is set to C<_source>.

=item * C<destination>

This directory will be created if it does not exist. Compiled and output files
are placed in this directory. By default, it is set to C<_destination>.

=back

=head2 C<compile>

Each file in the L</C<source>> directory is checked to see if it has a
registered convertor as well as a YAML configuration at the beginning. All such
files are compiled considering the L</YAML Configuration Keys> and the compiled
output is placed in the L</C<destination>> directory. The rest of the files are
copied over to the L</C<destination>> without compiling.

=head2 YAML Configuration Keys

L</YAML Configuration Keys> should be placed at the beginning of the file and
should be enclosed within a pair of C<--->.

Example of using a layout along with a custom key and compiling a markdown
L</C<source>> file:

L</layout> file - C<main.html>:

    <!DOCTYPE html>
    <html>
        <head>
            <title></title>
        </head>
        <body>
            [% content %]
        </body>
    </html>

L</source> file - C<index.markdown>:

    ---
    layout: main.html
    drink : water
    ---
    Drink **plenty** of [% drink %].

L</destination> (output/compiled) file - C<index.html>:

    <!DOCTYPE html>
    <html>
        <head>
            <title></title>
        </head>
        <body>
            <p>Drink <strong>plenty</strong> of water.</p>

        </body>
    </html>

=over 4

=item * C<layout>

Uses the corresponding layout or wrapper to wrap the compiled content. Note that
C<content> is a special variable used in C<L<Template Toolkit|Template>> along
with wrappers. This variable contains the processed wrapped content. In essence,
the output/compiled file will have the C<content> variable replaced with the
compiled L</C<source>> file.

=item * C<I<custom keys>>

These keys will be available for use in the same page as well as in the layout.
In the above example, C<drink> is a custom key.

=back

=head1 Walkthrough

Consider the source file C<index.markdown> which contains:

    ---
    layout : main.html
    title  : Just an example title
    heading: StaticVolt Example
    ---

    StaticVolt Example
    ==================

    This is an **example** page.

Let C<main.html> which is a wrapper or layout contain:

    <!DOCTYPE html>
    <html>
        <head>
            <title>[% title %]</title>
        </head>
        <body>
            [% content %]
        </body>
    </html>

During compilation, all variables defined as L</YAML Configuration Keys> at the
beginning of the file will be processed and be replaced by their values in the
output file C<index.html>. A registered convertor
(C<L<StaticVolt::Convertor::Markdown>>) is used to convert the markdown text to
HTML.

Compiled output file C<index.html> contains:

    <!DOCTYPE html>
    <html>
        <head>
            <title>Just an example title</title>
        </head>
        <body>
            <h1>StaticVolt Example</h1>
            <p>This is an <strong>example</strong> page.</p>

        </body>
    </html>

=head1 Default Convertors

=over 4

=item * C<L<StaticVolt::Convertor::Markdown>>

=item * C<L<StaticVolt::Convertor::Textile>>

=back

=head1 How to build a convertor?

The convertor should inherit from L<C<StaticVolt::Convertor>>. Define a
subroutine named C<L<StaticVolt::Convertor/convert>> that takes a single argument. This argument should
be converted to HTML and returned.

Register filename extensions by calling the C<register> method inherited from
L<C<StaticVolt::Convertor>>. C<register> accepts a list of filename extensions.

A convertor template that implements conversion from a hypothetical format
I<FooBar>:

    package StaticVolt::Convertor::FooBar;

    use strict;
    use warnings;

    use base qw( StaticVolt::Convertor );

    use Foo::Bar qw( foobar );

    sub convert {
        my $content = shift;
        return foobar $content;
    }

    # Handle files with the extensions:
    #   .foobar, .fb, .fbar, .foob
    __PACKAGE__->register(qw/ foobar fb fbar foob /);

=head1 Inspiration

L<StaticVolt> is inspired by Tom Preston-Werner's L<Jekyll|http://jekyllrb.com/>.

=head1 Acknowledgements

L<Shlomi Fish|http://www.shlomifish.org/> for suggesting change of licence.

=head1 See Also

L<Template Toolkit|Template>

=head1 AUTHOR

Alan Haggai Alavi <alanhaggai@alanhaggai.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2011 by Alan Haggai Alavi.

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)

=cut


__END__