Added CompassFilter.

This commit is contained in:
Jannis Leidel
2011-05-25 17:10:33 +02:00
parent be0922a9f2
commit d0411eba02
18 changed files with 284 additions and 70 deletions

3
.gitignore vendored
View File

@@ -10,4 +10,5 @@ MANIFEST
*.egg *.egg
docs/_build/ docs/_build/
.coverage .coverage
htmlcov htmlcov
.sass-cache

View File

@@ -130,7 +130,7 @@ class Compressor(object):
"mimetype '%s'." % mimetype) "mimetype '%s'." % mimetype)
else: else:
return CompilerFilter(content, filter_type=self.type, return CompilerFilter(content, filter_type=self.type,
command=command, filename=filename).output(**kwargs) command=command, filename=filename).input(**kwargs)
return content return content
def filter(self, content, method, **kwargs): def filter(self, content, method, **kwargs):

View File

@@ -40,6 +40,10 @@ class CompressorSettings(AppSettings):
YUI_JS_ARGUMENTS = '' YUI_JS_ARGUMENTS = ''
DATA_URI_MIN_SIZE = 1024 DATA_URI_MIN_SIZE = 1024
COMPASS_BINARY = 'compass'
COMPASS_ARGUMENTS = ' --no-line-comments --output-style expanded'
COMPASS_PLUGINS = []
# the cache backend to use # the cache backend to use
CACHE_BACKEND = None CACHE_BACKEND = None
# rebuilds the cache every 30 days if nothing has changed. # rebuilds the cache every 30 days if nothing has changed.

View File

@@ -1,21 +1,25 @@
import os
import logging import logging
import subprocess import subprocess
import tempfile import tempfile
from django.utils.datastructures import SortedDict
from compressor.conf import settings from compressor.conf import settings
from compressor.exceptions import FilterError from compressor.exceptions import FilterError
from compressor.utils import cmd_split, stringformat from compressor.utils import cmd_split
from compressor.utils.stringformat import FormattableString as fstr
logger = logging.getLogger("compressor.filters") logger = logging.getLogger("compressor.filters")
class FilterBase(object): class FilterBase(object):
def __init__(self, content, filter_type=None, verbose=0): def __init__(self, content, filter_type=None, filename=None, verbose=0):
self.type = filter_type self.type = filter_type
self.content = content self.content = content
self.verbose = verbose or settings.COMPRESS_VERBOSE self.verbose = verbose or settings.COMPRESS_VERBOSE
self.logger = logger self.logger = logger
self.filename = filename
def input(self, **kwargs): def input(self, **kwargs):
raise NotImplementedError raise NotImplementedError
@@ -30,40 +34,53 @@ class CompilerFilter(FilterBase):
external commands. external commands.
""" """
command = None command = None
filename = None options = ()
options = {}
def __init__(self, content, command=None, filename=None, *args, **kwargs): def __init__(self, content, command=None, *args, **kwargs):
super(CompilerFilter, self).__init__(content, *args, **kwargs) super(CompilerFilter, self).__init__(content, *args, **kwargs)
self.cwd = None
if command: if command:
self.command = command self.command = command
if self.command is None: if self.command is None:
raise FilterError("Required attribute 'command' not given") raise FilterError("Required attribute 'command' not given")
self.filename = filename if isinstance(self.options, dict):
new_options = ()
for item in kwargs.iteritems():
new_options += (item,)
self.options = new_options
for item in kwargs.iteritems():
self.options += (item,)
self.stdout = subprocess.PIPE self.stdout = subprocess.PIPE
self.stdin = subprocess.PIPE self.stdin = subprocess.PIPE
self.stderr = subprocess.PIPE self.stderr = subprocess.PIPE
self.infile, self.outfile = None, None
def output(self, **kwargs): def input(self, **kwargs):
infile = None options = dict(self.options)
outfile = None if self.infile is None:
try:
if "{infile}" in self.command: if "{infile}" in self.command:
infile = tempfile.NamedTemporaryFile(mode='w') if self.filename is None:
infile.write(self.content) self.infile = tempfile.NamedTemporaryFile(mode="w")
infile.flush() self.infile.write(self.content)
self.options["infile"] = self.filename or infile.name self.infile.flush()
if "{outfile}" in self.command: os.fsync(self.infile)
ext = ".%s" % self.type and self.type or "" options["infile"] = self.infile.name
outfile = tempfile.NamedTemporaryFile(mode='rw', suffix=ext) else:
self.options["outfile"] = outfile.name self.infile = open(self.filename)
command = stringformat.FormattableString(self.command) options["infile"] = self.filename
proc = subprocess.Popen(cmd_split(command.format(**self.options)),
stdout=self.stdout, stdin=self.stdin, stderr=self.stderr) if "{outfile}" in self.command and not "outfile" in options:
if infile is not None: ext = ".%s" % self.type and self.type or ""
filtered, err = proc.communicate() self.outfile = tempfile.NamedTemporaryFile(mode='r+', suffix=ext)
else: options["outfile"] = self.outfile.name
try:
command = fstr(self.command).format(**options)
proc = subprocess.Popen(cmd_split(command), shell=os.name=='nt',
stdout=self.stdout, stdin=self.stdin, stderr=self.stderr, cwd=self.cwd)
if self.infile is None:
filtered, err = proc.communicate(self.content) filtered, err = proc.communicate(self.content)
else:
filtered, err = proc.communicate()
except (IOError, OSError), e: except (IOError, OSError), e:
raise FilterError('Unable to apply %s (%r): %s' % raise FilterError('Unable to apply %s (%r): %s' %
(self.__class__.__name__, self.command, e)) (self.__class__.__name__, self.command, e))
@@ -75,11 +92,13 @@ class CompilerFilter(FilterBase):
raise FilterError(err) raise FilterError(err)
if self.verbose: if self.verbose:
self.logger.debug(err) self.logger.debug(err)
if outfile is not None: outfile_path = options.get('outfile')
filtered = outfile.read() if outfile_path:
self.outfile = open(outfile_path, 'r')
finally: finally:
if infile is not None: if self.infile is not None:
infile.close() self.infile.close()
if outfile is not None: if self.outfile is not None:
outfile.close() filtered = self.outfile.read()
self.outfile.close()
return filtered return filtered

View File

@@ -4,7 +4,7 @@ from compressor.filters import CompilerFilter
class ClosureCompilerFilter(CompilerFilter): class ClosureCompilerFilter(CompilerFilter):
command = "{binary} {args}" command = "{binary} {args}"
options = { options = (
"binary": settings.COMPRESS_CLOSURE_COMPILER_BINARY, ("binary", settings.COMPRESS_CLOSURE_COMPILER_BINARY),
"args": settings.COMPRESS_CLOSURE_COMPILER_ARGUMENTS, ("args", settings.COMPRESS_CLOSURE_COMPILER_ARGUMENTS),
} )

View File

@@ -0,0 +1,37 @@
import tempfile
from os import path
from compressor.conf import settings
from compressor.filters import CompilerFilter
class CompassFilter(CompilerFilter):
"""
Converts Compass files to css.
"""
command = "{binary} compile --force --quiet --boring {args} "
options = (
("binary", settings.COMPRESS_COMPASS_BINARY),
("args", settings.COMPRESS_COMPASS_ARGUMENTS),
)
def input(self, *args, **kwargs):
if self.filename is None:
self.filename = kwargs.pop('filename')
tmpdir = tempfile.mkdtemp()
parentdir = path.abspath(path.dirname(self.filename))
self.cwd = path.dirname(parentdir)
self.infile = open(self.filename)
outfile_name = path.splitext(path.split(self.filename)[1])[0] + '.css'
self.options += (
('infile', self.filename),
('tmpdir', tmpdir),
('sassdir', parentdir),
('outfile', path.join(tmpdir, outfile_name)),
('imagedir', settings.COMPRESS_URL),
)
for plugin in settings.COMPRESS_COMPASS_PLUGINS:
self.command += ' --require %s'% plugin
self.command += (' --sass-dir {sassdir} --css-dir {tmpdir}'
' --image-dir {imagedir} {infile}')
return super(CompassFilter, self).input(*args, **kwargs)

View File

@@ -4,7 +4,7 @@ from compressor.filters import CompilerFilter
class CSSTidyFilter(CompilerFilter): class CSSTidyFilter(CompilerFilter):
command = "{binary} {infile} {args} {outfile}" command = "{binary} {infile} {args} {outfile}"
options = { options = (
"binary": settings.COMPRESS_CSSTIDY_BINARY, ("binary", settings.COMPRESS_CSSTIDY_BINARY),
"args": settings.COMPRESS_CSSTIDY_ARGUMENTS, ("args", settings.COMPRESS_CSSTIDY_ARGUMENTS),
} )

View File

@@ -14,15 +14,15 @@ class YUICompressorFilter(CompilerFilter):
class YUICSSFilter(YUICompressorFilter): class YUICSSFilter(YUICompressorFilter):
type = 'css' type = 'css'
options = { options = (
"binary": settings.COMPRESS_YUI_BINARY, ("binary", settings.COMPRESS_YUI_BINARY),
"args": settings.COMPRESS_YUI_CSS_ARGUMENTS, ("args", settings.COMPRESS_YUI_CSS_ARGUMENTS),
} )
class YUIJSFilter(YUICompressorFilter): class YUIJSFilter(YUICompressorFilter):
type = 'js' type = 'js'
options = { options = (
"binary": settings.COMPRESS_YUI_BINARY, ("binary", settings.COMPRESS_YUI_BINARY),
"args": settings.COMPRESS_YUI_JS_ARGUMENTS, ("args", settings.COMPRESS_YUI_JS_ARGUMENTS),
} )

View File

@@ -71,6 +71,7 @@ class CompressorNode(template.Node):
return cache_content return cache_content
# 4. call compressor output method and handle exceptions # 4. call compressor output method and handle exceptions
rendered_output = compressor.output(self.mode, forced=forced)
try: try:
rendered_output = compressor.output(self.mode, forced=forced) rendered_output = compressor.output(self.mode, forced=forced)
if cache_key: if cache_key:

View File

@@ -0,0 +1,24 @@
# Require any additional compass plugins here.
# Set this to the root of your project when deployed:
http_path = "/"
css_dir = "stylesheets"
sass_dir = "sass"
images_dir = "images"
javascripts_dir = "javascripts"
# You can select your preferred output style here (can be overridden via the command line):
# output_style = :expanded or :nested or :compact or :compressed
# To enable relative paths to assets via compass helper functions. Uncomment:
# relative_assets = true
# To disable debugging comments that display the original location of your selectors. Uncomment:
# line_comments = false
# If you prefer the indented syntax, you might want to regenerate this
# project again passing --syntax sass, or you can uncomment this:
# preferred_syntax = :sass
# and then run:
# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass

View File

@@ -0,0 +1,5 @@
/* Welcome to Compass. Use this file to write IE specific override styles.
* Import this file using the following HTML or equivalent:
* <!--[if IE]>
* <link href="/stylesheets/ie.css" media="screen, projection" rel="stylesheet" type="text/css" />
* <![endif]--> */

View File

@@ -0,0 +1,3 @@
/* Welcome to Compass. Use this file to define print styles.
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/print.css" media="print" rel="stylesheet" type="text/css" /> */

View File

@@ -0,0 +1,6 @@
/* Welcome to Compass.
* In this file you should write your main styles. (or centralize your imports)
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" /> */
@import "compass/reset";

View File

@@ -0,0 +1,5 @@
/* Welcome to Compass. Use this file to write IE specific override styles.
* Import this file using the following HTML or equivalent:
* <!--[if IE]>
* <link href="/stylesheets/ie.css" media="screen, projection" rel="stylesheet" type="text/css" />
* <![endif]--> */

View File

@@ -0,0 +1,3 @@
/* Welcome to Compass. Use this file to define print styles.
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/print.css" media="print" rel="stylesheet" type="text/css" /> */

View File

@@ -0,0 +1,69 @@
/* Welcome to Compass.
* In this file you should write your main styles. (or centralize your imports)
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" /> */
/* line 17, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* line 20, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
body {
line-height: 1;
}
/* line 22, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
ol, ul {
list-style: none;
}
/* line 24, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
table {
border-collapse: collapse;
border-spacing: 0;
}
/* line 26, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
caption, th, td {
text-align: left;
font-weight: normal;
vertical-align: middle;
}
/* line 28, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
q, blockquote {
quotes: none;
}
/* line 101, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
q:before, q:after, blockquote:before, blockquote:after {
content: "";
content: none;
}
/* line 30, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
a img {
border: none;
}
/* line 115, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}

View File

@@ -23,9 +23,8 @@ def main():
content = content.replace('background:', 'color:') content = content.replace('background:', 'color:')
if options.outfile: if options.outfile:
f = open(options.outfile, 'w') with open(options.outfile, 'w') as f:
f.write(content) f.write(content)
f.close()
else: else:
print content print content

View File

@@ -483,43 +483,81 @@ color: black;
""" """
from compressor.filters.csstidy import CSSTidyFilter from compressor.filters.csstidy import CSSTidyFilter
self.assertEqual( self.assertEqual(
"font,th,td,p{color:#000;}", CSSTidyFilter(content).output()) "font,th,td,p{color:#000;}", CSSTidyFilter(content).input())
CssTidyTestCase = skipIf( CssTidyTestCase = skipIf(
find_command(settings.COMPRESS_CSSTIDY_BINARY) is None, find_command(settings.COMPRESS_CSSTIDY_BINARY) is None,
'CSStidy binary %r not found' % settings.COMPRESS_CSSTIDY_BINARY 'CSStidy binary %r not found' % settings.COMPRESS_CSSTIDY_BINARY
)(CssTidyTestCase) )(CssTidyTestCase)
class CompassTestCase(TestCase):
def setUp(self):
self.old_debug = settings.DEBUG
self.old_compress_css_filters = settings.COMPRESS_CSS_FILTERS
self.old_compress_url = settings.COMPRESS_URL
self.old_enabled = settings.COMPRESS_ENABLED
settings.DEBUG = True
settings.COMPRESS_ENABLED = True
settings.COMPRESS_CSS_FILTERS = [
'compressor.filters.compass.CompassFilter',
'compressor.filters.css_default.CssAbsoluteFilter',
]
settings.COMPRESS_URL = '/media/'
def tearDown(self):
settings.DEBUG = self.old_debug
settings.COMPRESS_URL = self.old_compress_url
settings.COMPRESS_ENABLED = self.old_enabled
settings.COMPRESS_CSS_FILTERS = self.old_compress_css_filters
def test_compass(self):
template = u"""{% load compress %}{% compress css %}
<link rel="stylesheet" href="{{ MEDIA_URL }}sass/screen.scss" type="text/css" charset="utf-8">
<link rel="stylesheet" href="{{ MEDIA_URL }}sass/print.scss" type="text/css" charset="utf-8">
{% endcompress %}
"""
context = {'MEDIA_URL': settings.COMPRESS_URL}
out = u'<link rel="stylesheet" href="/media/CACHE/css/3f807af2259c.css" type="text/css">'
self.assertEqual(out, render(template, context))
CompassTestCase = skipIf(
find_command(settings.COMPRESS_COMPASS_BINARY) is None,
'Compass binary %r not found' % settings.COMPRESS_COMPASS_BINARY
)(CompassTestCase)
class PrecompilerTestCase(TestCase): class PrecompilerTestCase(TestCase):
def setUp(self): def setUp(self):
self.this_dir = os.path.dirname(__file__) self.this_dir = os.path.dirname(__file__)
self.filename = os.path.join(self.this_dir, 'media/css/one.css') self.filename = os.path.join(self.this_dir, 'media/css/one.css')
self.test_precompiler = os.path.join(self.this_dir, 'precompiler.py')
with open(self.filename) as f: with open(self.filename) as f:
self.content = f.read() self.content = f.read()
self.test_precompiler = os.path.join(self.this_dir, 'precompiler.py')
def test_precompiler_infile_outfile(self): def test_precompiler_infile_outfile(self):
command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler) command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command) compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
self.assertEqual(u"body { color:#990; }", compiler.output()) self.assertEqual(u"body { color:#990; }", compiler.input())
def test_precompiler_stdin_outfile(self):
command = '%s %s -o {outfile}' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=None, command=command)
self.assertEqual(u"body { color:#990; }", compiler.output())
def test_precompiler_stdin_stdout(self):
command = '%s %s' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=None, command=command)
self.assertEqual(u"body { color:#990; }\n", compiler.output())
def test_precompiler_stdin_stdout_filename(self):
command = '%s %s' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
self.assertEqual(u"body { color:#990; }\n", compiler.output())
def test_precompiler_infile_stdout(self): def test_precompiler_infile_stdout(self):
command = '%s %s -f {infile}' % (sys.executable, self.test_precompiler) command = '%s %s -f {infile}' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=None, command=command) compiler = CompilerFilter(content=self.content, filename=None, command=command)
self.assertEqual(u"body { color:#990; }\n", compiler.output()) self.assertEqual(u"body { color:#990; }\n", compiler.input())
def test_precompiler_stdin_outfile(self):
command = '%s %s -o {outfile}' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=None, command=command)
self.assertEqual(u"body { color:#990; }", compiler.input())
def test_precompiler_stdin_stdout(self):
command = '%s %s' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=None, command=command)
self.assertEqual(u"body { color:#990; }\n", compiler.input())
def test_precompiler_stdin_stdout_filename(self):
command = '%s %s' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
self.assertEqual(u"body { color:#990; }\n", compiler.input())