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