line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Mojolicious::Plugin::AssetPack::Pipe::Sass; |
2
|
1
|
|
|
1
|
|
421
|
use Mojo::Base 'Mojolicious::Plugin::AssetPack::Pipe'; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
5
|
|
3
|
|
|
|
|
|
|
|
4
|
1
|
|
|
1
|
|
118
|
use Mojolicious::Plugin::AssetPack::Util qw(checksum diag dumper load_module DEBUG); |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
50
|
|
5
|
1
|
|
|
1
|
|
6
|
use Mojo::File; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
27
|
|
6
|
1
|
|
|
1
|
|
4
|
use Mojo::JSON qw(decode_json encode_json); |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
36
|
|
7
|
1
|
|
|
1
|
|
4
|
use Mojo::Util; |
|
1
|
|
|
|
|
8
|
|
|
1
|
|
|
|
|
1436
|
|
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
my $FORMAT_RE = qr{^s[ac]ss$}; |
10
|
|
|
|
|
|
|
my $IMPORT_RE = qr{ (?:^|[\n\r]+) ([^\@\r\n]*) (\@import \s+ (["']) (.*?) \3 \s* ;)}sx; |
11
|
|
|
|
|
|
|
my $SOURCE_MAP_PLACEHOLDER = sprintf '__%s__', __PACKAGE__; |
12
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
$SOURCE_MAP_PLACEHOLDER =~ s!::!_!g; |
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
has functions => sub { +{} }; |
16
|
|
|
|
|
|
|
has generate_source_map => sub { shift->app->mode eq 'development' ? 1 : 0 }; |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
sub process { |
19
|
0
|
|
|
0
|
1
|
|
my ($self, $assets) = @_; |
20
|
0
|
|
|
|
|
|
my $store = $self->assetpack->store; |
21
|
0
|
|
|
|
|
|
my %opts = (include_paths => [undef, @{$self->assetpack->store->paths}]); |
|
0
|
|
|
|
|
|
|
22
|
0
|
|
|
|
|
|
my $file; |
23
|
|
|
|
|
|
|
|
24
|
0
|
|
|
|
|
|
for my $name (keys %{$self->functions}) { |
|
0
|
|
|
|
|
|
|
25
|
0
|
|
|
|
|
|
my $cb = $self->functions->{$name}; |
26
|
0
|
|
|
0
|
|
|
$opts{sass_functions}{$name} = sub { $self->$cb(@_); }; |
|
0
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
} |
28
|
|
|
|
|
|
|
|
29
|
0
|
0
|
|
|
|
|
if ($self->generate_source_map) { |
30
|
0
|
|
|
|
|
|
$opts{source_map_file} = $SOURCE_MAP_PLACEHOLDER; |
31
|
0
|
0
|
|
|
|
|
$opts{source_map_file_urls} = $self->app->mode eq 'development' ? 1 : 0; |
32
|
|
|
|
|
|
|
} |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
return $assets->each( |
35
|
|
|
|
|
|
|
sub { |
36
|
0
|
|
|
0
|
|
|
my ($asset, $index) = @_; |
37
|
|
|
|
|
|
|
|
38
|
0
|
0
|
|
|
|
|
return if $asset->format !~ $FORMAT_RE; |
39
|
0
|
|
|
|
|
|
my ($attrs, $content) = ($asset->TO_JSON, $asset->content); |
40
|
0
|
|
|
|
|
|
local $self->{checksum_for_file} = {}; |
41
|
0
|
|
|
|
|
|
local $opts{include_paths}[0] = _include_path($asset); |
42
|
0
|
|
|
|
|
|
$attrs->{minified} = $self->assetpack->minify; |
43
|
0
|
0
|
|
|
|
|
$attrs->{key} = sprintf 'sass%s', $attrs->{minified} ? '-min' : ''; |
44
|
0
|
|
|
|
|
|
$attrs->{format} = 'css'; |
45
|
0
|
|
|
|
|
|
$attrs->{checksum} = $self->_checksum(\$content, $asset, $opts{include_paths}); |
46
|
|
|
|
|
|
|
|
47
|
0
|
0
|
|
|
|
|
return $asset->content($file)->FROM_JSON($attrs) if $file = $store->load($attrs); |
48
|
0
|
0
|
|
|
|
|
return if $asset->isa('Mojolicious::Plugin::AssetPack::Asset::Null'); |
49
|
0
|
0
|
|
|
|
|
$opts{include_paths}[0] = $asset->path ? $asset->path->dirname : undef; |
50
|
0
|
|
|
|
|
|
$opts{include_paths} = [grep {$_} @{$opts{include_paths}}]; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
51
|
0
|
|
|
|
|
|
diag 'Process "%s" with checksum %s.', $asset->url, $attrs->{checksum} if DEBUG; |
52
|
|
|
|
|
|
|
|
53
|
0
|
0
|
0
|
|
|
|
if ($self->{has_module} //= eval { load_module 'CSS::Sass'; 1 }) { |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
54
|
0
|
|
|
|
|
|
$opts{output_style} = _output_style($attrs->{minified}); |
55
|
0
|
0
|
|
|
|
|
$content = CSS::Sass::sass2scss($content) if $asset->format eq 'sass'; |
56
|
0
|
|
|
|
|
|
my ($css, $err, $stats) = CSS::Sass::sass_compile($content, %opts); |
57
|
0
|
0
|
|
|
|
|
if ($err) { |
58
|
0
|
|
|
|
|
|
die sprintf '[Pipe::Sass] Could not compile "%s" with opts=%s: %s', |
59
|
|
|
|
|
|
|
$asset->url, dumper(\%opts), $err; |
60
|
|
|
|
|
|
|
} |
61
|
0
|
|
|
|
|
|
$css = Mojo::Util::encode('UTF-8', $css); |
62
|
|
|
|
|
|
|
$self->_add_source_map_asset($asset, \$css, $stats) |
63
|
0
|
0
|
|
|
|
|
if $stats->{source_map_string}; |
64
|
0
|
|
|
|
|
|
$asset->content($store->save(\$css, $attrs))->FROM_JSON($attrs); |
65
|
|
|
|
|
|
|
} |
66
|
|
|
|
|
|
|
else { |
67
|
0
|
|
|
|
|
|
my @args = (qw(sass -s), map { ('-I', $_) } @{$opts{include_paths}}); |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
68
|
0
|
0
|
|
|
|
|
push @args, '--scss' if $asset->format eq 'scss'; |
69
|
0
|
|
|
|
|
|
$self->run(\@args, \$content, \my $css, undef); |
70
|
0
|
|
|
|
|
|
$asset->content($store->save(\$css, $attrs))->FROM_JSON($attrs); |
71
|
|
|
|
|
|
|
} |
72
|
|
|
|
|
|
|
} |
73
|
0
|
|
|
|
|
|
); |
74
|
|
|
|
|
|
|
} |
75
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
sub _add_source_map_asset { |
77
|
0
|
|
|
0
|
|
|
my ($self, $asset, $css, $stats) = @_; |
78
|
0
|
|
|
|
|
|
my $data = decode_json $stats->{source_map_string}; |
79
|
0
|
|
|
|
|
|
my $source_map = Mojolicious::Plugin::AssetPack::Asset->new( |
80
|
|
|
|
|
|
|
url => sprintf('%s.css.map', $asset->name)); |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
# override "stdin" with real file |
83
|
0
|
0
|
|
|
|
|
$data->{file} = sprintf 'file://%s', $asset->path if $asset->path; |
84
|
0
|
|
|
|
|
|
$data->{sources}[0] = $data->{file}; |
85
|
0
|
|
|
|
|
|
$source_map->content(encode_json $data); |
86
|
|
|
|
|
|
|
|
87
|
0
|
|
|
|
|
|
my $relative = join '/', '..', $source_map->checksum, $source_map->url; |
88
|
0
|
|
|
|
|
|
$$css =~ s!$SOURCE_MAP_PLACEHOLDER!$relative!; |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
# TODO |
91
|
0
|
|
|
|
|
|
$self->assetpack->{by_checksum}{$source_map->checksum} = $source_map; |
92
|
0
|
|
|
|
|
|
$self->assetpack->{by_topic}{$source_map->url} = Mojo::Collection->new($source_map); |
93
|
|
|
|
|
|
|
} |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
sub _checksum { |
96
|
0
|
|
|
0
|
|
|
my ($self, $ref, $asset, $paths) = @_; |
97
|
0
|
|
|
|
|
|
my $ext = $asset->format; |
98
|
0
|
|
|
|
|
|
my $store = $self->assetpack->store; |
99
|
0
|
|
|
|
|
|
my @c = (checksum $$ref); |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
SEARCH: |
102
|
0
|
|
|
|
|
|
while ($$ref =~ /$IMPORT_RE/gs) { |
103
|
0
|
|
|
|
|
|
my $pre = $1; |
104
|
0
|
|
|
|
|
|
my $rel_path = $4; |
105
|
0
|
|
|
|
|
|
my $mlen = length $2; |
106
|
0
|
|
|
|
|
|
my @rel = split '/', $rel_path; |
107
|
0
|
|
|
|
|
|
my $name = pop @rel; |
108
|
0
|
|
|
|
|
|
my $start = pos($$ref) - $mlen; |
109
|
0
|
|
|
|
|
|
my $dynamic = $rel_path =~ m!http://local/!; |
110
|
0
|
|
|
|
|
|
my @basename = ("_$name", $name); |
111
|
|
|
|
|
|
|
|
112
|
0
|
0
|
|
|
|
|
next if $pre =~ m{^\s*//}; |
113
|
|
|
|
|
|
|
|
114
|
|
|
|
|
|
|
# Follow sass rules for skipping, |
115
|
|
|
|
|
|
|
# ...with exception for special assetpack handling for dynamic sass include |
116
|
0
|
0
|
|
|
|
|
next if $rel_path =~ /\.css$/; |
117
|
0
|
0
|
0
|
|
|
|
next if $rel_path =~ m!^https?://! and !$dynamic; |
118
|
|
|
|
|
|
|
|
119
|
0
|
0
|
|
|
|
|
unshift @basename, "_$name.$ext", "$name.$ext" unless $name =~ /\.$ext$/; |
120
|
0
|
0
|
|
|
|
|
my $imported = $store->asset([map { join '/', @rel, $_ } @basename], $paths) |
|
0
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
or die qq([Pipe::Sass] Could not find "$rel_path" file in @$paths); |
122
|
|
|
|
|
|
|
|
123
|
0
|
0
|
|
|
|
|
if ($imported->path) { |
124
|
0
|
|
|
|
|
|
diag '@import "%s" (%s)', $rel_path, $imported->path if DEBUG >= 2; |
125
|
0
|
|
|
|
|
|
local $paths->[0] = _include_path($imported); |
126
|
0
|
|
|
|
|
|
push @c, $self->_checksum(\$imported->content, $imported, $paths); |
127
|
|
|
|
|
|
|
} |
128
|
|
|
|
|
|
|
else { |
129
|
0
|
|
|
|
|
|
diag '@import "%s" (memory)', $rel_path if DEBUG >= 2; |
130
|
0
|
|
|
|
|
|
pos($$ref) = $start; |
131
|
0
|
|
|
|
|
|
substr $$ref, $start, $mlen, |
132
|
|
|
|
|
|
|
$imported->content; # replace "@import ..." with content of asset |
133
|
0
|
|
|
|
|
|
push @c, $imported->checksum; |
134
|
|
|
|
|
|
|
} |
135
|
|
|
|
|
|
|
} |
136
|
|
|
|
|
|
|
|
137
|
0
|
|
|
|
|
|
return checksum join ':', @c; |
138
|
|
|
|
|
|
|
} |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
sub _include_path { |
141
|
0
|
|
|
0
|
|
|
my $asset = shift; |
142
|
0
|
0
|
|
|
|
|
return $asset->url if $asset->url =~ m!^https?://!; |
143
|
0
|
0
|
|
|
|
|
return $asset->path->dirname if $asset->path; |
144
|
0
|
|
|
|
|
|
return ''; |
145
|
|
|
|
|
|
|
} |
146
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
sub _install_sass { |
148
|
0
|
|
|
0
|
|
|
my $self = shift; |
149
|
0
|
|
|
|
|
|
$self->run([qw(ruby -rubygems -e), 'puts Gem.user_dir'], undef, \my $base); |
150
|
0
|
|
|
|
|
|
chomp $base; |
151
|
0
|
|
|
|
|
|
my $path = Mojo::File->new($base, qw(bin sass)); |
152
|
0
|
0
|
|
|
|
|
return $path if -e $path; |
153
|
0
|
|
|
|
|
|
$self->app->log->warn( |
154
|
|
|
|
|
|
|
'Installing sass... Please wait. (gem install --user-install sass)'); |
155
|
0
|
|
|
|
|
|
$self->run([qw(gem install --user-install sass)]); |
156
|
0
|
|
|
|
|
|
return $path; |
157
|
|
|
|
|
|
|
} |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
sub _output_style { |
160
|
0
|
0
|
|
0
|
|
|
return $_[0] ? CSS::Sass::SASS_STYLE_COMPRESSED() : CSS::Sass::SASS_STYLE_NESTED(); |
161
|
|
|
|
|
|
|
} |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
1; |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
=encoding utf8 |
166
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
=head1 NAME |
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
Mojolicious::Plugin::AssetPack::Pipe::Sass - Process sass and scss files |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
=head1 SYNOPSIS |
172
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
=head2 Application |
174
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
plugin AssetPack => {pipes => [qw(Sass Css Combine)]}; |
176
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
$self->pipe("Sass")->functions({ |
178
|
|
|
|
|
|
|
q[image-url($arg)] => sub { |
179
|
|
|
|
|
|
|
my ($pipe, $arg) = @_; |
180
|
|
|
|
|
|
|
return sprintf "url(/assets/%s)", $_[1]; |
181
|
|
|
|
|
|
|
} |
182
|
|
|
|
|
|
|
}); |
183
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
=head2 Sass file |
185
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
The sass file below shows how to use the custom "image-url" function: |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
body { |
189
|
|
|
|
|
|
|
background: #fff image-url('img.png') top left; |
190
|
|
|
|
|
|
|
} |
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
=head1 DESCRIPTION |
193
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
L will process sass and scss files. |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
This module require either the optional module L or the C |
197
|
|
|
|
|
|
|
program to be installed. C will be automatically installed using |
198
|
|
|
|
|
|
|
L unless already available. |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
=head1 ATTRIBUTES |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
=head2 functions |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
$hash_ref = $self->functions; |
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
Used to define custom SASS functions. Note that the functions will be called |
207
|
|
|
|
|
|
|
with C<$self> as the first argument, followed by any arguments from the SASS |
208
|
|
|
|
|
|
|
function. This invocation is EXPERIMENTAL, but will hopefully not change. |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
This attribute requires L to work. It will not get passed on to |
211
|
|
|
|
|
|
|
the C executable. |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
See L for example. |
214
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
=head2 generate_source_map |
216
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
$bool = $self->generate_source_map; |
218
|
|
|
|
|
|
|
$self = $self->generate_source_map(1); |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
This pipe will generate source maps if true. Default is "1" if |
221
|
|
|
|
|
|
|
L is "development". |
222
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
See also L and |
224
|
|
|
|
|
|
|
L for more |
225
|
|
|
|
|
|
|
information about the usefulness. |
226
|
|
|
|
|
|
|
|
227
|
|
|
|
|
|
|
See also Mojolicious::Plugin::AssetPack::Guides::Developing/Faster development |
228
|
|
|
|
|
|
|
cycle> for how to reload the page when changes are done inside the browser's |
229
|
|
|
|
|
|
|
dev tools. |
230
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
=head1 METHODS |
232
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
=head2 process |
234
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
See L. |
236
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
=head1 SEE ALSO |
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
L. |
240
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
=cut |