The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl
####################################################################################################
# sass (scss) compiler
####################################################################################################

use utf8;
use strict;
use warnings;

####################################################################################################
# dependencies
####################################################################################################

# parse options
use Pod::Usage;
use Getopt::Long;

# make the options case sensitive
Getopt::Long::Configure("bundling");

# load constants from libsass
use CSS::Sass qw(SASS_STYLE_EXPANDED);
use CSS::Sass qw(SASS_STYLE_NESTED);
use CSS::Sass qw(SASS_STYLE_COMPRESSED);
use CSS::Sass qw(SASS_STYLE_COMPACT);
use CSS::Sass::Watchdog qw(start_watchdog);
use CSS::Sass::Plugins qw(%plugins);

# setup encodings for std streams
# not sure why this is not default
binmode(STDIN, ":encoding(console_in)");
binmode(STDOUT, ":encoding(console_out)");
binmode(STDERR, ":encoding(console_out)");

####################################################################################################
# normalize command arguments to utf8
####################################################################################################

# get cmd arg encoding
use Encode::Locale qw();
# convert cmd args to utf8
use Encode qw(decode encode);
# now just decode every command arguments
@ARGV = map { decode(locale => $_, 1) } @ARGV;

####################################################################################################
# options variables
####################################################################################################

# init options
my $watchdog;
my $benchmark;
my $precision;
my $output_file;
my $output_style;
my $source_comments;
my $source_map_file;
my $source_map_root;
my $source_map_embed;
my $source_map_contents;
my $source_map_file_urls;
my $omit_source_map_url;

# paths arrays
my @plugin_paths;
my @include_paths;

# output styles
my $indent = "  ";
my $linefeed = "auto";

####################################################################################################
####################################################################################################

# define a sub to print out the version (mimic behaviour of node.js blessc)
# this script has it's own version numbering as it's not dependent on any libs
sub version {
	printf "psass %s (perl sass/scss compiler)\n", "0.5.0";
	printf "  libsass: %s\n", CSS::Sass::libsass_version();
	printf "  sass2scss: %s\n", CSS::Sass::sass2scss_version();
exit 0 };

sub list_plugins {

	foreach my $plugin (sort keys %plugins) {
		printf "--%s-plugin\n", $plugin;
	}

exit 0 };

####################################################################################################
####################################################################################################

my %options = (
	'help|h' => sub { pod2usage(1); },
	'watch|w!' => \ $watchdog,
	'version|v' => \ &version,
	'benchmark|b!' => \ $benchmark,
	'indent=s' => \ $indent,
	'linefeed=s' => \ $linefeed,
	'precision|p=s' => \ $precision,
	'output-file|o=s' => \ $output_file,
	'output-style|t=s' => \ $output_style,
	'line-numbers|c!' => \ $source_comments,
	'line-comments|c!' => \ $source_comments,
	'source-comments|c!' => \ $source_comments,
	'source-map-file|m=s' => \ $source_map_file,
	'source-map-root=s' => \ $source_map_root,
	'source-map-embed|e!' => \ $source_map_embed,
	'source-map-contents|s!' => \ $source_map_contents,
	'source-map-file-urls!' => \$source_map_file_urls,
	'no-source-map-url!' => \ $omit_source_map_url,
	'plugin-path|P|L=s' => sub { push @plugin_paths, $_[1] },
	'include-path|I=s' => sub { push @include_paths, $_[1] }
);

my %enable_plugins;
# add shortcuts for known plugins
foreach my $plugin (keys %plugins) {
	$enable_plugins{$plugin} = 0;
	$options{"${plugin}-plugin!"} =
		\ $enable_plugins{$plugin};
}
$options{'list-plugins'} = sub { list_plugins };
$options{'all-plugins'} = sub {
	foreach my $plugin (keys %plugins) {
		$enable_plugins{$plugin} = 1;
	}
};

# get options
GetOptions %options;

# register the enabled plugin paths
foreach my $plugin (keys %enable_plugins) {
	next unless $enable_plugins{$plugin};
	push @plugin_paths, $plugins{$plugin};
}

# set default if not configured
unless (defined $output_style)
{ $output_style = SASS_STYLE_NESTED }

# parse string to constant
elsif ($output_style =~ m/^n/i)
{ $output_style = SASS_STYLE_NESTED }
elsif ($output_style =~ m/^compa/i)
{ $output_style = SASS_STYLE_COMPACT }
elsif ($output_style =~ m/^compr/i)
{ $output_style = SASS_STYLE_COMPRESSED }
elsif ($output_style =~ m/^e/i)
{ $output_style = SASS_STYLE_EXPANDED }
# die with message if style is unknown
else { die "unknown output style: $output_style" }

# resolve linefeed options
if ($linefeed =~ m/^a/i)
{ $linefeed = undef; }
elsif ($linefeed =~ m/^w/i)
{ $linefeed = "\r\n"; }
elsif ($linefeed =~ m/^[u]/i)
{ $linefeed = "\n"; }
elsif ($linefeed =~ m/^[n]/i)
{ $linefeed = ""; }
# die with message if linefeed type is unknown
else { die "unknown linefeed type: $linefeed" }

# do we have output path in second arg?
if (defined $ARGV[1] && $ARGV[1] ne '-')
{ $output_file = $ARGV[1]; }

# check if the benchmark module is available
if ($benchmark && ! eval "use Benchmark; 1" )
{ die "Error loading Benchmark module\n", $@; }

####################################################################################################
# get sass standard option list
####################################################################################################

sub sass_options ()
{
	return (
		dont_die => $watchdog,
		indent => $indent,
		linefeed => $linefeed,
		precision => $precision,
		output_path => $output_file,
		output_style  => $output_style,
		plugin_paths => \ @plugin_paths,
		include_paths => \ @include_paths,
		source_comments => $source_comments,
		source_map_file => $source_map_file,
		source_map_root => $source_map_root,
		source_map_embed => $source_map_embed,
		source_map_contents => $source_map_contents,
		source_map_file_urls => $source_map_file_urls,
		omit_source_map_url => $omit_source_map_url,
	);
}

####################################################################################################
# implement our own safe File::Slurp::write_file
# since syswrite() is deprecated on :utf8 handles 
####################################################################################################

# To mark FILEHANDLE as UTF-8, use :utf8 or :encoding(UTF-8) . :utf8 just marks the
# data as UTF-8 without further checking, while :encoding(UTF-8) checks the data for
# actually being valid UTF-8. More details can be found in PerlIO::encoding.
sub write_file ($$)
{
	use Fcntl qw(:flock LOCK_EX);
	# my ($file, $content, $binmode) = @_; 
	# avoid arg copies and use @_ directly
	if (open (my $fh, ">", $_[0])) {
		unless (flock($fh, LOCK_EX)) {
			Carp::croak "Error aquiring file lock!\n",
				"Path: ", $_[0], "\n",
				"Reason: ", $!, "\n";
		}
		unless (binmode($fh, $_[2] || ':utf8')) {
			Carp::croak "Error setting file mode!\n",
				"Path: ", $_[0], "\n",
				"Reason: ", $!, "\n";
		}
		unless (print $fh $_[1]) {
			Carp::croak "Error writing output!\n",
				"Path: ", $_[0], "\n",
			 	"Reason: ", $!, "\n";
		}
	}
	else {
		Carp::croak "Error opening writable file!\n",
			"Path: ", $_[0], "\n",
			"Reason: ", $!, "\n";
	}
}

####################################################################################################
use CSS::Sass qw(sass_compile_file sass_compile);
####################################################################################################

# first run we always want to die on error
# because we will not get any included files
our $error = sub { die @_ };

sub compile ()
{
	# variables
	my ($css, $err, $stats);

	# get benchmark stamp before compiling
	my $t0 = $benchmark ? Benchmark->new : 0;

	# open filehandle if path is given
	if (defined $ARGV[0] && $ARGV[0] ne '-')
	{
		($css, $err, $stats) = sass_compile_file(
			$ARGV[0], sass_options()
		);
	}
	# or use standard input
	else
	{
		($css, $err, $stats) = sass_compile(
			join('', <STDIN>), sass_options()
		);
	}

	# get benchmark stamp after compiling
	my $t1 = $benchmark ? Benchmark->new : 0;
	# only print benchmark result when module is available
	if ($benchmark) { warn timestr(timediff($t1, $t0), 'auto', '5.4f'), "\n"; }

	# process return status values
	if (defined $css)
	{
		# by default we just print to standard out
		unless (defined $output_file) { print $css; }
		# or if output_file is defined via options we write it there
		else { write_file($output_file, $css ); }
	}
	elsif (defined $err) { $error->($err); }
	else { $error->("fatal error - aborting"); }

	# output source-map
	if ($source_map_file)
	{
		my $smap = $stats->{'source_map_string'};
		unless ($smap) { $error->("source-map not generated <$source_map_file>") }
		else { write_file($source_map_file, $smap ); }
	}

	# return according to expected return type
	return wantarray ? ($css, $err, $stats) : $css;
}

####################################################################################################
# main program execution
####################################################################################################

my ($css, $err, $stats) = compile();

if ($watchdog)
{
	local $error = sub { warn @_ };
	start_watchdog($stats, \&compile);
}

####################################################################################################
####################################################################################################

__END__

=head1 NAME

psass - perl sass (scss) compiler

=head1 SYNOPSIS

psass [options] [ path_in | - ] [ path_out | - ]

 Options:
   -v, --version                 print version
   -h, --help                    print this help
   -w, --watch                   start watchdog mode
   -p, --precision=int           precision for float output
       --indent=string           set indent string used for output
       --linefeed=type           linefeed used for output [auto|unix|win|none]
   -o, --output-file=file        output file to write result to
   -t, --output-style=style      output style [expanded|nested|compressed|compact]
   -P, --plugin-path=path        plugin load path (repeatable)
   -I, --include-path=path       sass include path (repeatable)
   -c, --source-comments         enable source debug comments
   -l, --line-comments           synonym for --source-comments
       --line-numbers            synonym for --source-comments
   -e, --source-map-embed        embed source-map in mapping url
   -s, --source-map-contents     include original contents
   -m, --source-map-file=file    create and write source-map to file
       --source-map-file-urls    create file urls for source paths
       --source-map-root=.       specific root for relative paths
       --no-source-map-url       omit sourceMappingUrl from output
       --benchmark               print benchmark for compilation time

   Plugins may be pre-installed by CSS::Sass or from 3rd parties.
   There are some options available for each known plugin.

       --all-plugins             enables all known plugins
       --list-plugin             print list of all known plugins
       --[name]-plugin           enables the plugin with [name]
       --no-[name]-plugin        disabled the plugin with [name]

=head1 OPTIONS

=over 8

=item B<-help>

Print a brief help message with options and exits.

=back

=head1 DESCRIPTION

B<This program> is a sass (scss) compiler

=cut