The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Dancer::Plugin::Preprocess::Markdown;

use strict;
use warnings;

# ABSTRACT: Generate HTML content from Markdown files

our $VERSION = '0.021'; # VERSION

use Cwd 'abs_path';
use Dancer ':syntax';
use Dancer::Plugin;
use File::Spec::Functions qw(catfile file_name_is_absolute);
use Text::Markdown qw(markdown);

my $settings = {
    layout => setting('layout'),
    # TODO: It might make more sense to have 1 as the default
    recursive => 0,
    save => 0,
    %{plugin_setting()}
};
my $paths;

if (exists $settings->{paths}) {
    $paths = $settings->{paths};
}

my $paths_re = join '|', map {
    my $s = $_;
    $s =~ s{^[^/]}{/$&};    # Add leading slash, if missing
    $s =~ s{/$}{};          # Remove trailing slash
    quotemeta $s;
} reverse sort keys %$paths;

sub _process_markdown_file {
    my $md_file = shift;

    open (my $f, '<', $md_file);
    my $contents;
    {
        local $/;
        $contents = <$f>;
    }
    close($f);

    return markdown($contents);
}

my $handler_defined;

# Postpone setting up the route handler to the time before the first request is
# processed, so that other routes defined in the app will take precedence.
hook on_reset_state => sub {
    return if $handler_defined;

    get qr{($paths_re)/(.*)} => sub {
        my ($path, $file) = splat;

        $path .= '/';
        my $path_settings;

        for my $path_prefix (reverse sort keys %$paths) {
            (my $path_prefix_slash = $path_prefix) =~ s{([^/])$}{$1/};

            if (substr($path, 0, length($path_prefix_slash)) eq
                $path_prefix_slash)
            {
                # Found a matching path
                $path_settings = {
                    # Top-level settings
                    layout => $settings->{layout},
                    recursive => $settings->{recursive},
                    save => $settings->{save},
                    # Path-specific settings (may override top-level ones)
                    %{$paths->{$path_prefix} || {}}
                };
                last;
            }
        }

        # Pass if there was no matching path
        return pass if (!defined $path_settings);

        # Pass if the requested file appears to be in a subdirectory while
        # recursive mode is off
        return pass if (!$path_settings->{recursive} && $file =~ m{/});

        if (!exists $path_settings->{src_dir}) {
            # Source directory not specified -- use default
            $path_settings->{src_dir} = path 'md', 'src', split(m{/}, $path);
        }

        # Strip off the ".html" suffix, if present
        $file =~ s/\.html$//;

        my $src_file;

        if (file_name_is_absolute($path_settings->{src_dir})) {
            $src_file = path $path_settings->{src_dir}, ($file . '.md');
        }
        else {
            # Assume a non-absolute source directory is relative to appdir
            $src_file = path abs_path(setting('appdir')),
                $path_settings->{src_dir}, ($file . '.md');
        }

        if (!-r $src_file) {
            return send_error("Not allowed", 403);
        }

        my $content;

        if ($path_settings->{save}) {
            if (!exists $path_settings->{dest_dir}) {
                $path_settings->{dest_dir} = path 'md', 'dest',
                    split(m{/}, $path);
            }

            my $dest_file;

            if (file_name_is_absolute($path_settings->{dest_dir})) {
                $dest_file = path $path_settings->{dest_dir}, ($file . '.html');
            }
            else {
                # Assume a non-absolute destination directory is relative to
                # appdir
                $dest_file = path abs_path(setting('appdir')),
                    $path_settings->{dest_dir}, ($file . '.html');
            }

            if (!-f $dest_file ||
                ((stat($dest_file))[9] < (stat($src_file))[9]))
            {
                # Source file is newer than destination file (or the latter does
                # not exist)
                $content = _process_markdown_file($src_file);

                if (open(my $f, '>', $dest_file)) {
                    print {$f} $content;
                    close($f);
                }
                else {
                    warning __PACKAGE__ .
                        ": Can't open '$dest_file' for writing";
                }
            }
            else {
                # The HTML file already exists -- read its contents back to the
                # client
                if (open (my $f, '<', $dest_file)) {
                    local $/;
                    $content = <$f>;
                    close($f);
                }
                else {
                    warning __PACKAGE__ .
                        ": Can't open '$dest_file' for reading";
                }
            }
        }

        if (!defined $content) {
            $content = _process_markdown_file($src_file); 
        }

        # TODO: Add support for path-specific layouts
        return (engine 'template')->apply_layout($content, {},
            { layout => $path_settings->{layout} });
    };

    $handler_defined = 1;
};

register_plugin;

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Dancer::Plugin::Preprocess::Markdown - Generate HTML content from Markdown files

=head1 VERSION

version 0.021

=head1 SYNOPSIS

Dancer::Plugin::Preprocess::Markdown automatically generates HTML content from
Markdown files in a Dancer web application.

Add the plugin to your application:

    use Dancer::Plugin::Preprocess::Markdown;

Configure its settings in the YAML configuration file:

    plugins:
      "Preprocess::Markdown":
        save: 1
        paths:
          "/documents":
            recursive: 1
            save: 0
          "/articles":
            src_dir: "articles/markdown"
            dest_dir: "articles/html"
            layout: "article"

=head1 DESCRIPTION

Dancer::Plugin::Preprocess::Markdown generates HTML content from Markdown source
files.

When an HTML file is requested, and its path matches one of the paths specified
in the configuration, the plugin looks for a corresponding Markdown file and
processes it to produce the HTML content. The generated HTML file may then be
saved and re-used with subsequent requests for the same URL.

=head1 CONFIGURATION

The available configuration settings are described below.

=head2 Top-level settings

=head3 layout

The layout used to display the generated HTML content.

=head3 paths

A collection of paths that will be served by the plugin. Each path entry may
define path-specific settings that override top-level settings.

=head3 recursive

If set to C<0>, then the plugin only processes files placed in the source
directory and not its subdirectories. If set to C<1>, subdirectories are also
processed.

Default: C<0>

=head3 save

If set to C<0>, then the HTML content is generated on-the-fly with every
request. If set to C<1>, then HTML files are generated once and saved, and are
used in subsequent responses. The files are regenerated every time the source
Markdown files are modified.

Default: C<0>

=head2 Path-specific settings

=head3 src_dir

The directory where source Markdown files are located.

Default: C<md/src/I<{path}>>

=head3 dest_dir

The destination directory for the generated HTML files (if the C<save> option is
in use).

Default: C<md/dest/I<{path}>>

=head1 ROUTE HANDLERS VS. PATHS

If there's a route defined in your application that matches one of the paths
served by the plugin, it will take precedence. For example, with the following
configuration:

    plugins:
      "Preprocess::Markdown":
        paths:
          "/documents":
            ...

and this route in the application:

    get '/documents/faq' => sub {
        ...
    };

A request for C</documents/faq> won't be processed by the plugin, but by the
handler defined in the application.

=head1 SEE ALSO

=over 4

=item *

L<Text::Markdown>

=item *

L<Markdown Homepage|http://daringfireball.net/projects/markdown/>

=back

=head1 ACKNOWLEDGEMENTS

Markdown to HTML conversion is done with L<Text::Markdown>, written by Tomas
Doran.

=cut

=for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan

=head1 SUPPORT

=head2 Bugs / Feature Requests

Please report any bugs or feature requests through the issue tracker
at L<https://github.com/odyniec/p5-Dancer-Plugin-Preprocess-Markdown/issues>.
You will be notified automatically of any progress on your issue.

=head2 Source Code

This is open source software.  The code repository is available for
public review and contribution under the terms of the license.

L<https://github.com/odyniec/p5-Dancer-Plugin-Preprocess-Markdown>

  git clone https://github.com/odyniec/p5-Dancer-Plugin-Preprocess-Markdown.git

=head1 AUTHOR

Michal Wojciechowski <odyniec@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2013 by Michal Wojciechowski.

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

=cut