File: Sass.pm

package info (click to toggle)
libmojolicious-plugin-assetpack-perl 2.15-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,888 kB
  • sloc: perl: 1,503; javascript: 52; makefile: 8; sh: 2
file content (235 lines) | stat: -rw-r--r-- 7,848 bytes parent folder | download
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
package Mojolicious::Plugin::AssetPack::Pipe::Sass;
use Mojo::Base 'Mojolicious::Plugin::AssetPack::Pipe';

use Mojolicious::Plugin::AssetPack::Util qw(checksum diag dumper load_module DEBUG);
use Mojo::File;
use Mojo::JSON qw(decode_json encode_json);
use Mojo::Util;

my $FORMAT_RE              = qr{^s[ac]ss$};
my $IMPORT_RE              = qr{ (?:^|[\n\r]+) ([^\@\r\n]*) (\@import \s+ (["']) (.*?) \3 \s* ;)}sx;
my $SOURCE_MAP_PLACEHOLDER = sprintf '__%s__', __PACKAGE__;

$SOURCE_MAP_PLACEHOLDER =~ s!::!_!g;

has functions           => sub { +{} };
has generate_source_map => sub { shift->app->mode eq 'development' ? 1 : 0 };

sub process {
  my ($self, $assets) = @_;
  my $store = $self->assetpack->store;
  my %opts  = (include_paths => [undef, @{$self->assetpack->store->paths}]);
  my $file;

  for my $name (keys %{$self->functions}) {
    my $cb = $self->functions->{$name};
    $opts{sass_functions}{$name} = sub { $self->$cb(@_); };
  }

  if ($self->generate_source_map) {
    $opts{source_map_file}      = $SOURCE_MAP_PLACEHOLDER;
    $opts{source_map_file_urls} = $self->app->mode eq 'development' ? 1 : 0;
  }

  return $assets->each(sub {
    my ($asset, $index) = @_;

    return if $asset->format !~ $FORMAT_RE;
    my ($attrs, $content) = ($asset->TO_JSON, $asset->content);
    local $self->{checksum_for_file} = {};
    local $opts{include_paths}[0] = _include_path($asset);
    $attrs->{minified} = $self->assetpack->minify;
    $attrs->{key}      = sprintf 'sass%s', $attrs->{minified} ? '-min' : '';
    $attrs->{format}   = 'css';
    $attrs->{checksum} = $self->_checksum(\$content, $asset, $opts{include_paths});

    return $asset->content($file)->FROM_JSON($attrs) if $file = $store->load($attrs);
    return if $asset->isa('Mojolicious::Plugin::AssetPack::Asset::Null');
    $opts{include_paths}[0] = $asset->path ? $asset->path->dirname : undef;
    $opts{include_paths} = [grep {$_} @{$opts{include_paths}}];
    diag 'Process "%s" with checksum %s.', $asset->url, $attrs->{checksum} if DEBUG;

    if ($self->{has_module} //= eval { load_module 'CSS::Sass'; 1 }) {
      $opts{output_style} = _output_style($attrs->{minified});
      $content = CSS::Sass::sass2scss($content) if $asset->format eq 'sass';
      my ($css, $err, $stats) = CSS::Sass::sass_compile($content, %opts);
      if ($err) {
        die sprintf '[Pipe::Sass] Could not compile "%s" with opts=%s: %s', $asset->url, dumper(\%opts), $err;
      }
      $css = Mojo::Util::encode('UTF-8', $css);
      $self->_add_source_map_asset($asset, \$css, $stats) if $stats->{source_map_string};
      $asset->content($store->save(\$css, $attrs))->FROM_JSON($attrs);
    }
    else {
      my @args = (qw(sass -s --trace), map { ('-I', $_) } @{$opts{include_paths}});
      push @args, '--scss'          if $asset->format eq 'scss';
      push @args, qw(-t compressed) if $attrs->{minified};
      $self->run(\@args, \$content, \my $css, \my $err);
      my $exit = $? > 0 ? $? >> 8 : $?;
      if ($exit) {
        die sprintf '[Pipe::Sass] Could not compile "%s" with opts=%s: %s', $asset->url, dumper(\%opts), $err;
      }
      $asset->content($store->save(\$css, $attrs))->FROM_JSON($attrs);
    }
  });
}

sub _add_source_map_asset {
  my ($self, $asset, $css, $stats) = @_;
  my $data       = decode_json $stats->{source_map_string};
  my $source_map = Mojolicious::Plugin::AssetPack::Asset->new(url => sprintf('%s.css.map', $asset->name));

  # override "stdin" with real file
  $data->{file}       = sprintf 'file://%s', $asset->path if $asset->path;
  $data->{sources}[0] = $data->{file};
  $source_map->content(encode_json $data);

  my $relative = join '/', '..', $source_map->checksum, $source_map->url;
  $$css =~ s!$SOURCE_MAP_PLACEHOLDER!$relative!;

  # TODO
  $self->assetpack->{by_checksum}{$source_map->checksum} = $source_map;
  $self->assetpack->{by_topic}{$source_map->url}         = Mojo::Collection->new($source_map);
}

sub _checksum {
  my ($self, $ref, $asset, $paths) = @_;
  my $ext   = $asset->format;
  my $store = $self->assetpack->store;
  my @c     = (checksum $$ref);

SEARCH:
  while ($$ref =~ /$IMPORT_RE/gs) {
    my $pre      = $1;
    my $rel_path = $4;
    my $mlen     = length $2;
    my @rel      = split '/', $rel_path;
    my $name     = pop @rel;
    my $start    = pos($$ref) - $mlen;
    my $dynamic  = $rel_path =~ m!http://local/!;
    my @basename = ("_$name", $name);

    next if $pre =~ m{^\s*//};

    # Follow sass rules for skipping,
    # ...with exception for special assetpack handling for dynamic sass include
    next if $rel_path =~ /\.css$/;
    next if $rel_path =~ m!^https?://! and !$dynamic;

    unshift @basename, "_$name.$ext", "$name.$ext" unless $name =~ /\.$ext$/;
    my $imported = $store->asset([map { join '/', @rel, $_ } @basename], $paths)
      or die qq([Pipe::Sass] Could not find "$rel_path" file in @$paths);

    if ($imported->path) {
      diag '@import "%s" (%s)', $rel_path, $imported->path if DEBUG >= 2;
      local $paths->[0] = _include_path($imported);
      push @c, $self->_checksum(\$imported->content, $imported, $paths);
    }
    else {
      diag '@import "%s" (memory)', $rel_path if DEBUG >= 2;
      pos($$ref) = $start;
      substr $$ref, $start, $mlen, $imported->content;    # replace "@import ..." with content of asset
      push @c, $imported->checksum;
    }
  }

  return checksum join ':', @c;
}

sub _include_path {
  my $asset = shift;
  return $asset->url           if $asset->url =~ m!^https?://!;
  return $asset->path->dirname if $asset->path;
  return '';
}

sub _install_sass {
  my $self = shift;
  $self->run([qw(ruby -rubygems -e), 'puts Gem.user_dir'], undef, \my $base);
  chomp $base;
  my $path = Mojo::File->new($base, qw(bin sass));
  return $path if -e $path;
  $self->app->log->warn('Installing sass... Please wait. (gem install --user-install sass)');
  $self->run([qw(gem install --user-install sass)]);
  return $path;
}

sub _output_style {
  return $_[0] ? CSS::Sass::SASS_STYLE_COMPRESSED() : CSS::Sass::SASS_STYLE_NESTED();
}

1;

=encoding utf8

=head1 NAME

Mojolicious::Plugin::AssetPack::Pipe::Sass - Process sass and scss files

=head1 SYNOPSIS

=head2 Application

  plugin AssetPack => {pipes => [qw(Sass Css Combine)]};

  $self->pipe("Sass")->functions({
    q[image-url($arg)] => sub {
      my ($pipe, $arg) = @_;
      return sprintf "url(/assets/%s)", $_[1];
    }
  });

=head2 Sass file

The sass file below shows how to use the custom "image-url" function:

  body {
    background: #fff image-url('img.png') top left;
  }

=head1 DESCRIPTION

L<Mojolicious::Plugin::AssetPack::Pipe::Sass> will process sass and scss files.

This module require either the optional module L<CSS::Sass> or the C<sass>
program to be installed. C<sass> will be automatically installed using
L<https://rubygems.org/> unless already available.

=head1 ATTRIBUTES

=head2 functions

  $hash_ref = $self->functions;

Used to define custom SASS functions. Note that the functions will be called
with C<$self> as the first argument, followed by any arguments from the SASS
function. This invocation is EXPERIMENTAL, but will hopefully not change.

This attribute requires L<CSS::Sass> to work. It will not get passed on to
the C<sass> executable.

See L</SYNOPSIS> for example.

=head2 generate_source_map

  $bool = $self->generate_source_map;
  $self = $self->generate_source_map(1);

This pipe will generate source maps if true. Default is "1" if
L<Mojolicious/mode> is "development".

See also L<http://thesassway.com/intermediate/using-source-maps-with-sass> and
L<https://robots.thoughtbot.com/sass-source-maps-chrome-magic> for more
information about the usefulness.

=head1 METHODS

=head2 process

See L<Mojolicious::Plugin::AssetPack::Pipe/process>.

=head1 SEE ALSO

L<Mojolicious::Plugin::AssetPack>.

=cut