1from __future__ import print_function
2import os, subprocess
3
4from webassets.filter import ExternalTool
5from webassets.cache import FilesystemCache
6
7
8__all__ = ('RubySass', 'RubySCSS')
9
10
11class RubySass(ExternalTool):
12    """Converts `Sass <http://sass-lang.com/>`_ markup to real CSS.
13
14    This filter uses the legacy ruby Sass compiler, which has been
15    replaced by the dart version in use in the ``sass`` filter.
16
17    Requires the Sass executable to be available externally. To install
18    it, you might be able to do::
19
20         $ sudo gem install sass
21
22    By default, this works as an "input filter", meaning ``sass`` is
23    called for each source file in the bundle. This is because the
24    path of the source file is required so that @import directives
25    within the Sass file can be correctly resolved.
26
27    However, it is possible to use this filter as an "output filter",
28    meaning the source files will first be concatenated, and then the
29    Sass filter is applied in one go. This can provide a speedup for
30    bigger projects.
31
32    To use Sass as an output filter::
33
34        from webassets.filter import get_filter
35        sass = get_filter('sass', as_output=True)
36        Bundle(...., filters=(sass,))
37
38    However, if you want to use the output filter mode and still also
39    use the @import directive in your Sass files, you will need to
40    pass along the ``load_paths`` argument, which specifies the path
41    to which the imports are relative to (this is implemented by
42    changing the working directory before calling the ``sass``
43    executable)::
44
45        sass = get_filter('sass', as_output=True, load_paths='/tmp')
46
47    With ``as_output=True``, the resulting concatenation of the Sass
48    files is piped to Sass via stdin (``cat ... | sass --stdin ...``)
49    and may cause applications to not compile if import statements are
50    given as relative paths.
51
52    For example, if a file ``foo/bar/baz.scss`` imports file
53    ``foo/bar/bat.scss`` (same directory) and the import is defined as
54    ``@import "bat";`` then Sass will fail compiling because Sass
55    has naturally no information on where ``baz.scss`` is located on
56    disk (since the data was passed via stdin) in order for Sass to
57    resolve the location of ``bat.scss``::
58
59        Traceback (most recent call last):
60        ...
61        webassets.exceptions.FilterError: sass: subprocess had error: stderr=(sass):1: File to import not found or unreadable: bat. (Sass::SyntaxError)
62               Load paths:
63                 /path/to/project-foo
64                on line 1 of standard input
65          Use --trace for backtrace.
66        , stdout=, returncode=65
67
68    To overcome this issue, the full path must be provided in the
69    import statement, ``@import "foo/bar/bat"``, then webassets
70    will pass the ``load_paths`` argument (e.g.,
71    ``/path/to/project-foo``) to Sass via its ``-I`` flags so Sass can
72    resolve the full path to the file to be imported:
73    ``/path/to/project-foo/foo/bar/bat``
74
75    Support configuration options:
76
77    SASS_BIN
78        The path to the Sass binary. If not set, the filter will
79        try to run ``sass`` as if it's in the system path.
80
81    SASS_STYLE
82        The style for the output CSS. Can be one of ``expanded`` (default),
83        ``nested``, ``compact`` or ``compressed``.
84
85    SASS_DEBUG_INFO
86        If set to ``True``, will cause Sass to output debug information
87        to be used by the FireSass Firebug plugin. Corresponds to the
88        ``--debug-info`` command line option of Sass.
89
90        Note that for this, Sass uses ``@media`` rules, which are
91        not removed by a CSS compressor. You will thus want to make
92        sure that this option is disabled in production.
93
94        By default, the value of this option will depend on the
95        environment ``DEBUG`` setting.
96
97    SASS_LINE_COMMENTS
98        Passes ``--line-comments`` flag to sass which emit comments in the
99        generated CSS indicating the corresponding source line.
100
101	Note that this option is disabled by Sass if ``--style compressed`` or
102        ``--debug-info`` options are provided.
103
104        Enabled by default. To disable, set empty environment variable
105        ``SASS_LINE_COMMENTS=`` or pass ``line_comments=False`` to this filter.
106
107    SASS_AS_OUTPUT
108        By default, this works as an "input filter", meaning ``sass`` is
109        called for each source file in the bundle. This is because the
110        path of the source file is required so that @import directives
111        within the Sass file can be correctly resolved.
112
113        However, it is possible to use this filter as an "output filter",
114        meaning the source files will first be concatenated, and then the
115        Sass filter is applied in one go. This can provide a speedup for
116        bigger projects.
117
118        It will also allow you to share variables between files.
119
120    SASS_SOURCE_MAP
121        If provided, this will generate source maps in the output depending
122	on the type specified. By default this will use Sass's ``auto``.
123	Possible values are ``auto``, ``file``, ``inline``, or ``none``.
124
125    SASS_LOAD_PATHS
126        It should be a list of paths relatives to Environment.directory or absolute paths.
127        Order matters as sass will pick the first file found in path order.
128        These are fed into the -I flag of the sass command and
129        is used to control where sass imports code from.
130
131    SASS_LIBS
132        It should be a list of paths relatives to Environment.directory or absolute paths.
133        These are fed into the -r flag of the sass command and
134        is used to require ruby libraries before running sass.
135    """
136    # TODO: If an output filter could be passed the list of all input
137    # files, the filter might be able to do something interesting with
138    # it (for example, determine that all source files are in the same
139    # directory).
140
141    name = 'sass_ruby'
142    options = {
143        'binary': 'SASS_BIN',
144        'use_scss': ('scss', 'SASS_USE_SCSS'),
145        'use_compass': ('use_compass', 'SASS_COMPASS'),
146        'debug_info': 'SASS_DEBUG_INFO',
147        'as_output': 'SASS_AS_OUTPUT',
148        'load_paths': 'SASS_LOAD_PATHS',
149        'libs': 'SASS_LIBS',
150        'style': 'SASS_STYLE',
151	'source_map': 'SASS_SOURCE_MAP',
152        'line_comments': 'SASS_LINE_COMMENTS',
153    }
154    max_debug_level = None
155
156    def resolve_path(self, path):
157        return self.ctx.resolver.resolve_source(self.ctx, path)
158
159    def _apply_sass(self, _in, out, cd=None):
160        # Switch to source file directory if asked, so that this directory
161        # is by default on the load path. We could pass it via -I, but then
162        # files in the (undefined) wd could shadow the correct files.
163        orig_cwd = os.getcwd()
164        child_cwd = orig_cwd
165        if cd:
166            child_cwd = cd
167
168        args = [self.binary or 'sass',
169                '--stdin',
170                '--style', self.style or 'expanded']
171        if self.line_comments is None or self.line_comments:
172            args.append('--line-comments')
173        if isinstance(self.ctx.cache, FilesystemCache):
174            args.extend(['--cache-location',
175                         os.path.join(orig_cwd, self.ctx.cache.directory, 'sass')])
176        elif not cd:
177            # Without a fixed working directory, the location of the cache
178            # is basically undefined, so prefer not to use one at all.
179            args.extend(['--no-cache'])
180        if (self.ctx.environment.debug if self.debug_info is None else self.debug_info):
181            args.append('--debug-info')
182        if self.use_scss:
183            args.append('--scss')
184        if self.use_compass:
185            args.append('--compass')
186        if self.source_map:
187            args.append('--sourcemap=' + self.source_map)
188        for path in self.load_paths or []:
189            if os.path.isabs(path):
190                abs_path = path
191            else:
192                abs_path = self.resolve_path(path)
193            args.extend(['-I', abs_path])
194        for lib in self.libs or []:
195            if os.path.isabs(lib):
196                abs_path = lib
197            else:
198                abs_path = self.resolve_path(lib)
199            args.extend(['-r', abs_path])
200
201        return self.subprocess(args, out, _in, cwd=child_cwd)
202
203    def input(self, _in, out, source_path, output_path, **kw):
204        if self.as_output:
205            out.write(_in.read())
206        else:
207            self._apply_sass(_in, out, os.path.dirname(source_path))
208
209    def output(self, _in, out, **kwargs):
210        if not self.as_output:
211            out.write(_in.read())
212        else:
213            self._apply_sass(_in, out)
214
215
216class RubySCSS(RubySass):
217    """Version of the ``sass`` filter that uses the SCSS syntax.
218    """
219
220    name = 'scss_ruby'
221
222    def __init__(self, *a, **kw):
223        assert not 'scss' in kw
224        kw['scss'] = True
225        super(RubySCSS, self).__init__(*a, **kw)
226