1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';
BEGIN {
use File::Basename qw(dirname);
use File::Spec;
my $d = dirname(File::Spec->rel2abs($0));
require "$d/../setenv.pl";
}
use CSS::Sass;
use File::ChangeNotify;
use File::Find::Rule;
use File::Slurp;
use Getopt::Long;
use MIME::Base64;
use MIME::Types;
use Path::Tiny;
use Pod::Usage;
# Store ARGV in case we need to restart later.
my @ARGVorig = @ARGV;
my %styles = (
nested => SASS_STYLE_NESTED,
expanded => SASS_STYLE_EXPANDED,
compact => SASS_STYLE_COMPACT,
compressed => SASS_STYLE_COMPRESSED,
);
my $style = 'compressed';
GetOptions(
'style|t=s' => sub { die "Unknown style\n" unless $_[1] =~ /nested|expanded|compact|compressed/; $style = $_[1]; },
'verbose' => \my $verbose,
'watch' => \my $watch,
'help|?' => \my $help,
) or pod2usage(2);
pod2usage(1) if $help;
my $mime_types = MIME::Types->new;
my $sass = CSS::Sass->new(
output_style => $styles{$style},
dont_die => 1,
sass_functions => {
'inline-image($url)' => sub {
my ($url) = @_;
die '$url should be a string' unless $url->isa("CSS::Sass::Value::String");
# URL is given with reference to the file, which we don't have here. Assume.
my $data = encode_base64(path("web/cobrands/fixmystreet/$url")->slurp_raw, "");
my $type = $mime_types->mimeTypeOf($url);
return "url('data:$type;base64,$data')";
}
},
);
# Get directories from the command line, defaulting to 'web' if none.
# We need absolute paths so that the include files lookup works.
my @dirs = map { m{/} ? $_ : "web/cobrands/$_" } @ARGV;
@dirs = 'web' unless @dirs;
@dirs = map { path($_)->absolute->stringify } @dirs;
# Initial compilation, to also discover all the included files.
my %includes;
my %include_to_main;
foreach my $file (File::Find::Rule->file->name(qr/^[^_].*\.scss$/)->in(@dirs)) {
my @includes = compile($file, $verbose);
$includes{$file} = \@includes;
map { push @{$include_to_main{$_}}, $file } @includes ? @includes : $file;
}
# If we're not watching, we're done!
exit unless $watch;
my $watcher = File::ChangeNotify->instantiate_watcher(
directories => [ @dirs, keys %include_to_main ],
filter => qr/\.scss$/,
);
say "\033[34mWatching for changes\033[0m";
while ( my @events = $watcher->wait_for_events() ) {
for my $file (map { $_->path } @events) {
verbose($file, "%s was updated");
for my $inc (@{$include_to_main{$file}}) {
my @includes = compile($inc, 1);
# From CSS::Sass::Watchdog test, we see includes are sorted
if (@includes && @{$includes{$inc}} && "@{$includes{$inc}}" ne "@includes") {
say "\033[34mRestarting to update includes\033[0m";
exec( $^X, $0, @ARGVorig ) or die "Can't re-exec myself($^X,$0): $!\n";
}
}
}
}
# Given a SCSS file, compile it and generate a .map file,
# show an error if any, and return the list of includes.
sub compile {
my ($file, $verbose) = @_;
(my $output_file = $file) =~ s/scss$/css/;
my $source_map_file = "$output_file.map";
$sass->options->{source_map_file} = $source_map_file;
my ($css, $stats) = $sass->compile_file($file);
unless ($css) {
warn "\033[31m" . $sass->last_error . "\033[0m";;
return;
}
my $written = write_if_different($output_file, $css);
if ($written) {
verbose($file, " \033[32mupdated\033[0m %s");
} elsif ($verbose) {
verbose($file, " \033[33munchanged\033[0m %s");
}
write_if_different($source_map_file, $stats->{source_map_string});
return @{$stats->{included_files} || []};
}
# Write a file, only if it has changed.
sub write_if_different {
my ($fn, $data) = @_;
my $current = File::Slurp::read_file($fn, { binmode => ':utf8', err_mode => 'quiet' });
if (!$current || $current ne $data) {
File::Slurp::write_file($fn, { binmode => ':utf8' }, $data);
return 1;
}
return 0;
}
sub verbose {
my ($file, $format) = @_;
# Strip most of file path, keep dir/file
(my $pr = $file) =~ s{.*/(.*/.*)\.scss}{$1};
say sprintf $format, $pr;
}
__END__
=head1 NAME
make_css - Generate CSS files from SCSS files, watch for changes.
=head1 SYNOPSIS
make_css [options] [dirs ...]
Options:
--verbose display more information
--watch wait for file updates and compile automatically
--style, -t CSS output style (one of nested, expanded, compact, compressed)
--help this help message
If no directories are specified, any .scss files under web/ that do not begin
with a "_" will be processed. "web/cobrands/" may be omitted from a given
directory.
=cut
|