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

use strict;
use warnings;

# ABSTRACT: Generate CSS files from Sass/SCSS files

our $VERSION = '0.031'; # VERSION

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

my $settings = plugin_setting;
my $paths;

my $sass = Text::Sass->new;
my $public_dir = abs_path(setting('public') || '');

if (exists $settings->{paths}) {
    $paths = $settings->{paths};
}
else {
    $paths = ['css'];
}

# Translate URL paths to filesystem paths
my @fs_paths = map { catfile(split('/'), "") } @$paths;

if ($settings->{save}) {
    # Check if the directories are writable
    for my $path (@fs_paths) {
        my $full_path = catfile($public_dir, $path);
        if (!-w $full_path) {
            warning __PACKAGE__ . ": Can't write to $full_path";
        }
    }
}

# Make a regular expression to match URL paths
my $paths_re = join '|', map {
    my $s = $_;
    $s =~ s{^[^/]}{/$&};    # Add leading slash, if missing
    $s =~ s{/$}{};          # Remove trailing slash
    quotemeta $s;
} reverse sort @$paths;

sub _process_sass_file {
    my $sass_file = shift;
    my $method;
    
    if ($sass_file =~ /\.sass$/) {
        $method = 'sass2css';
    }
    elsif ($sass_file =~ /\.scss$/) {
        $method = 'scss2css';
    }
    else {
        return;
    }
    
    open (my $f, '<', $sass_file);
    my $contents;
    {
        local $/;
        $contents = <$f>;
    }
    close($f);
    
    return $sass->$method($contents);
}

hook before_file_render => sub {
    # If saving is not enabled, then there's nothing for us to do (since we
    # won't be able to save the generated CSS rules in a file, duh)
    return if !$settings->{save};
    
    my $path = abs_path(shift);
    
    # Build a regular expression to match filesystem paths
    my $fs_paths_re = join '|', map { quotemeta } @fs_paths;
    
    my $path_re = '^' . quotemeta(catfile($public_dir, "")) .
        '(?:' . $fs_paths_re . ')';

    if ($path =~ qr{$path_re} && $path =~ qr{\.css$}) {
        (my $filename = $path) =~ s/\.css$//;
        my $input_file;
        
        if ((-f ($input_file = $filename . '.sass') ||
            -f ($input_file = $filename . '.scss')) &&
            (stat($path))[9] < (stat($input_file))[9])
        {
            # There is a Sass/Scss file newer than the CSS file
            my $css = _process_sass_file($input_file);
            
            if (defined $css) {
                # Save the generated CSS data
                open(my $f, '>', $path);
                print {$f} $css;
                close($f);
            }
        }
    }
};

get qr{($paths_re)/([^/]*\.css)} => sub {
    my ($path, $css_file) = splat;

    # Prepend the path to the CSS file name
    $path = catfile(split('/', $path));
    $css_file = catfile($path, $css_file);
    
    # Prepend public directory location
    my $css_file_abs = catfile($public_dir, $css_file);
    
    (my $filename = $css_file_abs) =~ s/\.css$//;
    my $input_file;

    if (-f ($input_file = $filename . '.sass') ||
        -f ($input_file = $filename . '.scss'))
    {
        # Sass/Scss file exists
        my $css = _process_sass_file($input_file);
        
        if ($settings->{save}) {
            # Saving enabled -- save the generated CSS as the requested file
            open(my $f, '>', $css_file_abs);
            print {$f} $css;
            close($f);
        }
        
        header 'content-type' => 'text/css';
        return $css;
    }
    else {
        # Check if the CSS file exists. Probably not, because if it does exist,
        # then Dancer should have already served it as a static file, and we
        # shouldn't even end up in this route handler. Still, we'll do this
        # check in case we're in some wacky parallel universe where route
        # handlers are run before static files.
        if (-f $css_file_abs) {
            return send_file($css_file);
        }
        else {
            return send_error("Not found", 404);
        }
    }
};

register_plugin;

1; # End of Dancer::Plugin::Preprocess::Sass

__END__

=pod

=encoding UTF-8

=head1 NAME

Dancer::Plugin::Preprocess::Sass - Generate CSS files from Sass/SCSS files

=head1 VERSION

version 0.031

=head1 SYNOPSIS

Dancer::Plugin::Preprocess::Sass adds support for Sass/SCSS files in a Dancer
web application.

Add the plugin to your application:

    use Dancer::Plugin::Preprocess::Sass;

Configure its settings in the YAML configuration file:

    plugins:
      "Preprocess::Sass":
        save: 1
        paths:
          - css
          - subdir/css

=head1 DESCRIPTION

Dancer::Plugin::Preprocess::Sass adds support for Sass/SCSS files in a Dancer
web application.

When a request is received for a CSS file, the plugin looks for a Sass/SCSS file
with the same name, and transforms it into CSS. The generated CSS file may then
be saved and served as a regular static file. Every time the source Sass/SCSS
file gets modified, the corresponding CSS file is regenerated.

=head1 CONFIGURATION

The available configuration settings are described below.

=head2 save

If set to C<0>, then the CSS files are generated on-the-fly with every request.
If set to C<1>, the files are generated once and saved, then served as static
files later on.

CSS files are saved in the same directory as the Sass/SCSS files, so the system
user that the web application is running as must be allowed to write to that
directory.

Default: C<0>

=head2 paths

A list of paths to serve CSS files from. Each path is relative to the C<public>
directory of the application.

    plugins:
      "Preprocess::Sass":
        paths:
          - css
          - subdir/css

Default: C<'css'>

=head1 SEE ALSO

=over 4



=back

* L<http://sass-lang.com/> - Sass website

=head1 ACKNOWLEDGEMENTS

The plugin uses Roger Pettett's L<Text::Sass> module. 

=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-Sass/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-Sass>

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

=head1 AUTHOR

Michal Wojciechowski <odyniec@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2011 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