"""
Generally speaking, compass provides a command line util that is used
a) as a management script (like django-admin.py) doing for example
setup work, adding plugins to a project etc), and
b) can compile the sass source files into CSS.
While generally project-based, starting with 0.10, compass supposedly
supports compiling individual files, which is what we are using for
implementing this filter. Supposedly, because there are numerous issues
that require working around. See the comments in the actual filter code
for the full story on all the hoops be have to jump through.
An alternative option would be to use Sass to compile. Compass essentially
adds two things on top of sass: A bunch of CSS frameworks, ported to Sass,
and available for including. And various ruby helpers that these frameworks
and custom Sass files can use. Apparently there is supposed to be a way
to compile a compass project through sass, but so far, I haven't got it
to work. The syntax is supposed to be one of:
$ sass -r compass `compass imports` FILE
$ sass --compass FILE
See:
http://groups.google.com/group/compass-users/browse_thread/thread/a476dfcd2b47653e
http://groups.google.com/group/compass-users/browse_thread/thread/072bd8b51bec5f7c
http://groups.google.com/group/compass-users/browse_thread/thread/daf55acda03656d1
"""
import os
from os import path
import tempfile
import shutil
import subprocess
from io import open
from webassets import six
from webassets.exceptions import FilterError
from webassets.filter import Filter, option
__all__ = ('Compass',)
class CompassConfig(dict):
"""A trivial dict wrapper that can generate a Compass config file."""
def to_string(self):
def string_rep(val):
""" Determine the correct string rep for the config file """
if isinstance(val, bool):
# True -> true and False -> false
return six.text_type(val).lower()
elif isinstance(val, six.string_types) and val.startswith(':'):
# ruby symbols, like :nested, used for "output_style"
return six.text_type(val)
elif isinstance(val, dict):
# ruby hashes, for "sass_options" for example
return u'{%s}' % ', '.join("'%s' => '%s'" % i for i in val.items())
elif isinstance(val, tuple):
val = list(val)
# works fine with strings and lists
return repr(val)
return u'\n'.join(['%s = %s' % (k, string_rep(v)) for k, v in self.items()])
class Compass(Filter):
"""Converts `Compass `_ .sass files to
CSS.
Requires at least version 0.10.
To compile a standard Compass project, you only need to have
to compile your main ``screen.sass``, ``print.sass`` and ``ie.sass``
files. All the partials that you include will be handled by Compass.
If you want to combine the filter with other CSS filters, make
sure this one runs first.
Supported configuration options:
COMPASS_BIN
The path to the Compass binary. If not set, the filter will
try to run ``compass`` as if it's in the system path.
COMPASS_PLUGINS
Compass plugins to use. This is equivalent to the ``--require``
command line option of the Compass. and expects a Python list
object of Ruby libraries to load.
COMPASS_CONFIG
An optional dictionary of Compass `configuration options
`_.
The values are emitted as strings, and paths are relative to the
Environment's ``directory`` by default; include a ``project_path``
entry to override this.
The ``sourcemap`` option has a caveat. A file called _.css.map is
created by Compass in the tempdir (where _.scss is the original asset),
which is then moved into the output_path directory. Since the tempdir
is created one level down from the output path, the relative links in
the sourcemap should correctly map. This file, however, will not be
versioned, and thus this option should ideally only be used locally
for development and not in production with a caching service as the
_.css.map file will not be invalidated.
"""
name = 'compass'
max_debug_level = None
options = {
'compass': ('binary', 'COMPASS_BIN'),
'plugins': option('COMPASS_PLUGINS', type=list),
'config': 'COMPASS_CONFIG',
}
def open(self, out, source_path, **kw):
"""Compass currently doesn't take data from stdin, and doesn't allow
us accessing the result from stdout either.
Also, there's a bunch of other issues we need to work around:
- compass doesn't support given an explict output file, only a
"--css-dir" output directory.
We have to "guess" the filename that will be created in that
directory.
- The output filename used is based on the input filename, and
simply cutting of the length of the "sass_dir" (and changing
the file extension). That is, compass expects the input
filename to always be inside the "sass_dir" (which defaults to
./src), and if this is not the case, the output filename will
be gibberish (missing characters in front). See:
https://github.com/chriseppstein/compass/issues/304
We fix this by setting the proper --sass-dir option.
- Compass insists on creating a .sass-cache folder in the
current working directory, and unlike the sass executable,
there doesn't seem to be a way to disable it.
The workaround is to set the working directory to our temp
directory, so that the cache folder will be deleted at the end.
"""
# Create temp folder one dir below output_path so sources in
# sourcemap are correct. This will be in the project folder,
# and as such, while exteremly unlikely, this could interfere
# with existing files and directories.
tempout_dir = path.normpath(
path.join(path.dirname(kw['output_path']), '../')
)
tempout = tempfile.mkdtemp(dir=tempout_dir)
# Temporarily move to "tempout", so .sass-cache will be created there
old_wd = os.getcwd()
os.chdir(tempout)
try:
# Make sure to use normpath() to not cause trouble with
# compass' simplistic path handling, where it just assumes
# source_path is within sassdir, and cuts off the length of
# sassdir from the input file.
sassdir = path.normpath(path.dirname(source_path))
source_path = path.normpath(source_path)
# Compass offers some helpers like image-url(), which need
# information about the urls under which media files will be
# available. This is hard for two reasons: First, the options in
# question aren't supported on the command line, so we need to write
# a temporary config file. Secondly, they assume defined and
# separate directories for "images", "stylesheets" etc., something
# webassets knows nothing of: we don't support the user defining
# such directories. Because we traditionally had this
# filter point all type-specific directories to the root media
# directory, we will define the paths to match this. In other
# words, in Compass, both inline-image("img/test.png) and
# image-url("img/test.png") will find the same file, and assume it
# to be {env.directory}/img/test.png.
# However, this partly negates the purpose of an utility like
# image-url() in the first place - you not having to hard code
# the location of your images. So we allow direct modification of
# the configuration file via the COMPASS_CONFIG setting (see
# tickets #36 and #125).
#
# Note that there is also the --relative-assets option, which we
# can't use because it calculates an actual relative path between
# the image and the css output file, the latter being in a
# temporary directory in our case.
config = CompassConfig(
project_path=self.ctx.directory,
http_path=self.ctx.url,
http_images_dir='',
http_stylesheets_dir='',
http_fonts_dir='',
http_javascripts_dir='',
images_dir='',
output_style=':expanded',
)
# Update with the custom config dictionary, if any.
if self.config:
config.update(self.config)
config_file = path.join(tempout, '.config.rb')
f = open(config_file, 'w')
try:
f.write(config.to_string())
f.flush()
finally:
f.close()
command = [self.compass or 'compass', 'compile']
for plugin in self.plugins or []:
command.extend(('--require', plugin))
command.extend(['--sass-dir', sassdir,
'--css-dir', tempout,
'--config', config_file,
'--quiet',
'--boring',
source_path])
proc = subprocess.Popen(command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
# shell: necessary on windows to execute
# ruby files, but doesn't work on linux.
shell=(os.name == 'nt'))
stdout, stderr = proc.communicate()
# compass seems to always write a utf8 header? to stderr, so
# make sure to not fail just because there's something there.
if proc.returncode != 0:
raise FilterError(('compass: subprocess had error: stderr=%s, '+
'stdout=%s, returncode=%s') % (
stderr, stdout, proc.returncode))
guessed_outputfilename = path.splitext(path.basename(source_path))[0]
guessed_outputfilepath = path.join(tempout, guessed_outputfilename)
output_file = open("%s.css" % guessed_outputfilepath, encoding='utf-8')
if config.get('sourcemap'):
sourcemap_file = open("%s.css.map" % guessed_outputfilepath)
sourcemap_output_filepath = path.join(
path.dirname(kw['output_path']),
path.basename(sourcemap_file.name)
)
if not path.exists(path.dirname(sourcemap_output_filepath)):
os.mkdir(path.dirname(sourcemap_output_filepath))
sourcemap_output_file = open(sourcemap_output_filepath, 'w')
sourcemap_output_file.write(sourcemap_file.read())
sourcemap_file.close()
try:
contents = output_file.read()
out.write(contents)
finally:
output_file.close()
finally:
# Restore previous working dir
os.chdir(old_wd)
# Clean up the temp dir
shutil.rmtree(tempout)