import os from webassets.filter import Filter from webassets.utils import working_directory __all__ = ('PyScss',) class PyScss(Filter): """Converts `Scss `_ markup to real CSS. This uses `PyScss `_, a native Python implementation of the Scss language. The PyScss module needs to be installed. It's API has been changing; currently, version 1.1.5 is known to be supported. This is an alternative to using the ``sass`` or ``scss`` filters, which are based on the original, external tools. .. note:: The Sass syntax is not supported by PyScss. You need to use the ``sass`` filter based on the original Ruby implementation instead. *Supported configuration options:* PYSCSS_DEBUG_INFO (debug_info) Include debug information in the output for use with FireSass. If unset, the default value will depend on your :attr:`Environment.debug` setting. PYSCSS_LOAD_PATHS (load_paths) Additional load paths that PyScss should use. .. warning:: The filter currently does not automatically use :attr:`Environment.load_path` for this. PYSCSS_STATIC_ROOT (static_root) The directory PyScss should look in when searching for include files that you have referenced. Will use :attr:`Environment.directory` by default. PYSCSS_STATIC_URL (static_url) The url PyScss should use when generating urls to files in ``PYSCSS_STATIC_ROOT``. Will use :attr:`Environment.url` by default. PYSCSS_ASSETS_ROOT (assets_root) The directory PyScss should look in when searching for things like images that you have referenced. Will use ``PYSCSS_STATIC_ROOT`` by default. PYSCSS_ASSETS_URL (assets_url) The url PyScss should use when generating urls to files in ``PYSCSS_ASSETS_ROOT``. Will use ``PYSCSS_STATIC_URL`` by default. PYSCSS_STYLE (style) The style of the output CSS. Can be one of ``nested`` (default), ``compact``, ``compressed``, or ``expanded``. """ # TODO: PyScss now allows STATIC_ROOT to be a callable, though # none of the other pertitent values are allowed to be, so this # is probably not good enough for us. name = 'pyscss' options = { 'debug_info': 'PYSCSS_DEBUG_INFO', 'load_paths': 'PYSCSS_LOAD_PATHS', 'static_root': 'PYSCSS_STATIC_ROOT', 'static_url': 'PYSCSS_STATIC_URL', 'assets_root': 'PYSCSS_ASSETS_ROOT', 'assets_url': 'PYSCSS_ASSETS_URL', 'style': 'PYSCSS_STYLE', } max_debug_level = None def setup(self): super(PyScss, self).setup() import scss self.scss = scss if self.style: try: from packaging.version import Version except ImportError: from distutils.version import LooseVersion as Version assert Version(scss.__version__) >= Version('1.2.0'), \ 'PYSCSS_STYLE only supported in pyScss>=1.2.0' # Initialize various settings: # Why are these module-level, not instance-level ?! # TODO: It appears that in the current dev version, the # settings can finally passed to a constructor. We'll need # to support this. # Only the dev version appears to support a list if self.load_paths: scss.config.LOAD_PATHS = ','.join(self.load_paths) # These are needed for various helpers (working with images # etc.). Similar to the compass filter, we require the user # to specify such paths relative to the media directory. try: scss.config.STATIC_ROOT = self.static_root or self.ctx.directory scss.config.STATIC_URL = self.static_url or self.ctx.url except EnvironmentError: raise EnvironmentError('Because Environment.url and/or ' 'Environment.directory are not set, you need to ' 'provide values for the PYSCSS_STATIC_URL and/or ' 'PYSCSS_STATIC_ROOT settings.') # This directory PyScss will use when generating new files, # like a spritemap. Maybe we should REQUIRE this to be set. scss.config.ASSETS_ROOT = self.assets_root or scss.config.STATIC_ROOT scss.config.ASSETS_URL = self.assets_url or scss.config.STATIC_URL def input(self, _in, out, **kw): """Like the original sass filter, this also needs to work as an input filter, so that relative @imports can be properly resolved. """ source_path = kw['source_path'] # Because PyScss always puts the current working dir at first # place of the load path, this is what we need to use to make # relative references work. with working_directory(os.path.dirname(source_path)): scss_opts = { 'debug_info': ( self.ctx.environment.debug if self.debug_info is None else self.debug_info), } if self.style: scss_opts['style'] = self.style else: scss_opts['compress'] = False scss = self.scss.Scss( scss_opts=scss_opts, # This is rather nice. We can pass along the filename, # but also give it already preprocessed content. scss_files={source_path: _in.read()}) # Compile # Note: This will not throw an error when certain things # are wrong, like an include file missing. It merely outputs # to stdout, via logging. We might have to do something about # this, and evaluate such problems to an exception. out.write(scss.compile())