Commits

Waldemar Kornewald committed 915c5dc

added lesscss support. thanks a lot to Wilfred Hughes for the patch!

Comments (0)

Files changed (2)

mediagenerator/filters/less.py

+from django.utils.encoding import smart_str
+from django.conf import settings
+from hashlib import sha1
+from mediagenerator.generators.bundles.base import Filter
+from mediagenerator.utils import find_file, read_text_file, get_media_dirs
+from subprocess import Popen, PIPE
+import os
+import sys
+import re
+import posixpath
+
+
+_RE_FLAGS = re.MULTILINE | re.UNICODE
+multi_line_comment_re = re.compile(r'/\*.*?\*/', _RE_FLAGS | re.DOTALL)
+one_line_comment_re = re.compile(r'//.*', _RE_FLAGS)
+import_re = re.compile(r'''@import\s*  # import keyword
+                           ["']        # opening quote
+                           (.+?)       # the module name
+                           ["']        # closing quote
+                           \s*;        # statement terminator
+                           ''',
+                       _RE_FLAGS | re.VERBOSE)
+
+if not hasattr(os.path, 'relpath'):
+    # backport os.path.relpath from Python 2.6
+    # Copyright (c) 2001-2010 Python Software Foundation; All Rights Reserved
+
+    # Return the longest prefix of all list elements.
+    def commonprefix(m):
+        "Given a list of pathnames, returns the longest common leading component"
+        if not m: return ''
+        s1 = min(m)
+        s2 = max(m)
+        for i, c in enumerate(s1):
+            if c != s2[i]:
+                return s1[:i]
+        return s1
+
+    def relpath(path, start=os.path.curdir):
+        """Return a relative version of a path"""
+
+        if not path:
+            raise ValueError("no path specified")
+
+        start_list = [x for x in os.path.abspath(start).split(os.path.sep) if x]
+        path_list = [x for x in os.path.abspath(path).split(os.path.sep) if x]
+
+        # Work out how much of the filepath is shared by start and path.
+        i = len(commonprefix([start_list, path_list]))
+
+        rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:]
+        if not rel_list:
+            return os.path.curdir
+        return os.path.join(*rel_list)
+
+    os.path.relpath = relpath
+
+
+class Less(Filter):
+    takes_input = False
+
+    def __init__(self, **kwargs):
+        self.config(kwargs, path=(), main_module=None)
+        if isinstance(self.path, basestring):
+            self.path = (self.path,)
+
+        # we need to be able to mutate self.path,
+        self.path = list(self.path)
+
+        super(Less, self).__init__(**kwargs)
+
+        assert self.filetype == 'css', (
+            'Less only supports compilation to CSS. '
+            'The parent filter expects "%s".' % self.filetype)
+        assert self.main_module, \
+            'You must provide a main module'
+
+        # lessc can't cope with nonexistent directories, so filter them
+        media_dirs = [directory for directory in get_media_dirs()
+                      if os.path.exists(directory)]
+        self.path += tuple(media_dirs)
+
+        self._compiled = None
+        self._compiled_hash = None
+        self._dependencies = {}
+
+    @classmethod
+    def from_default(cls, name):
+        return {'main_module': name}
+
+    def get_output(self, variation):
+        self._regenerate(debug=False)
+        yield self._compiled
+
+    def get_dev_output(self, name, variation):
+        assert name == self.main_module + '.css'
+        self._regenerate(debug=True)
+        return self._compiled
+
+    def get_dev_output_names(self, variation):
+        self._regenerate(debug=True)
+        yield self.main_module + '.css', self._compiled_hash
+
+    def _regenerate(self, debug=False):
+        if self._dependencies:
+            for name, mtime in self._dependencies.items():
+                path = self._find_file(name)
+                if not path or os.path.getmtime(path) != mtime:
+                    # Just recompile everything
+                    self._dependencies = {}
+                    break
+            else:
+                # No changes
+                return
+
+        modules = [self.main_module]
+        # get all the transitive dependencies of this module
+        while True:
+            if not modules:
+                break
+
+            module_name = modules.pop()
+            path = self._find_file(module_name)
+            assert path, 'Could not find the Less module %s' % module_name
+            mtime = os.path.getmtime(path)
+            self._dependencies[module_name] = mtime
+
+            source = read_text_file(path)
+            dependencies = self._get_dependencies(source)
+
+            for name in dependencies:
+                # Try relative import, first
+                transformed = posixpath.join(posixpath.dirname(module_name), name)
+                path = self._find_file(transformed)
+                if path:
+                    name = transformed
+                else:
+                    path = self._find_file(name)
+                assert path, ('The Less module %s could not find the '
+                              'dependency %s' % (module_name, name))
+                if name not in self._dependencies:
+                    modules.append(name)
+
+        main_module_path = self._find_file(self.main_module)
+        self._compiled = self._compile(main_module_path, debug=debug)
+        self._compiled_hash = sha1(smart_str(self._compiled)).hexdigest()
+
+    def _compile(self, path, debug=False):
+        try:
+            relative_paths = [self._get_relative_path(directory)
+                              for directory in self.path]
+
+            shell = sys.platform == 'win32'
+
+            cmd = Popen(['lessc',
+                         '--include-path=%s' % ':'.join(relative_paths),
+                         path],
+                        stdin=PIPE, stdout=PIPE, stderr=PIPE,
+                        shell=shell, universal_newlines=True,
+                        cwd=settings.PROJECT_ROOT)
+            output, error = cmd.communicate()
+
+            # some lessc errors output to stdout, so we put both in the assertion message
+            assert cmd.wait() == 0, ('Less command returned bad '
+                                     'result:\n%s\n%s' % (error, output))
+            return output.decode('utf-8')
+        except Exception, e:
+            raise ValueError("Failed to run Less compiler for this "
+                "file. Please confirm that the \"lessc\" application is "
+                "on your path and that you can run it from your own command "
+                "line.\n"
+                "Error was: %s" % e)
+
+    def _get_dependencies(self, source):
+        clean_source = multi_line_comment_re.sub('\n', source)
+        clean_source = one_line_comment_re.sub('', clean_source)
+
+        return [name for name in import_re.findall(clean_source)
+                if not name.endswith('.css')]
+
+    def _find_file(self, name):
+        if not name.endswith('.less'):
+            name = name + '.less'
+
+        return find_file(name, media_dirs=self.path)
+
+    def _get_relative_path(self, abs_path):
+        """Given an absolute path, return a path relative to the
+        project root.
+
+        >>> self._get_relative_path('/home/bob/bobs_project/subdir/foo')
+        'subdir/foo'
+
+        """
+        relative_path = os.path.relpath(abs_path, settings.PROJECT_ROOT)
+        return relative_path

mediagenerator/generators/bundles/settings.py

     'pyva': 'mediagenerator.filters.pyvascript_filter.PyvaScript',
     'sass': 'mediagenerator.filters.sass.Sass',
     'scss': 'mediagenerator.filters.sass.Sass',
+    'less': 'mediagenerator.filters.less.Less',
 })
 
 ROOT_MEDIA_FILTERS = getattr(settings, 'ROOT_MEDIA_FILTERS', {})