File Coverage

blib/lib/Mojolicious/Plugin/AssetPack/Pipe/Sass.pm
Criterion Covered Total %
statement 15 113 13.2
branch 0 46 0.0
condition 0 6 0.0
subroutine 5 13 38.4
pod 1 1 100.0
total 21 179 11.7


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