diff options
Diffstat (limited to 'django_pyscss')
| -rw-r--r-- | django_pyscss/__init__.py | 1 | ||||
| -rw-r--r-- | django_pyscss/compiler.py | 58 | ||||
| -rw-r--r-- | django_pyscss/compressor.py | 13 | ||||
| -rw-r--r-- | django_pyscss/extension/__init__.py | 0 | ||||
| -rw-r--r-- | django_pyscss/extension/django.py | 44 | ||||
| -rw-r--r-- | django_pyscss/scss.py | 223 | ||||
| -rw-r--r-- | django_pyscss/utils.py | 28 |
7 files changed, 136 insertions, 231 deletions
diff --git a/django_pyscss/__init__.py b/django_pyscss/__init__.py index e69de29..a31ff85 100644 --- a/django_pyscss/__init__.py +++ b/django_pyscss/__init__.py @@ -0,0 +1 @@ +from .compiler import DjangoScssCompiler # NOQA diff --git a/django_pyscss/compiler.py b/django_pyscss/compiler.py new file mode 100644 index 0000000..cf20595 --- /dev/null +++ b/django_pyscss/compiler.py @@ -0,0 +1,58 @@ +from __future__ import absolute_import + +import os +from pathlib import PurePath + +from django.utils.six.moves import StringIO + +from django.conf import settings +from django.contrib.staticfiles.storage import staticfiles_storage + +from scss import Compiler, config +from scss.extension.compass import CompassExtension +from scss.source import SourceFile + +from .extension.django import DjangoExtension +from .utils import find_all_files, get_file_and_storage + + +# TODO: It's really gross to modify this global settings variable. +# This is where PyScss is supposed to find the image files for making sprites. +config.STATIC_ROOT = find_all_files +config.STATIC_URL = staticfiles_storage.url('scss/') + +# This is where PyScss places the sprite files. +config.ASSETS_ROOT = os.path.join(settings.STATIC_ROOT, 'scss', 'assets') +# PyScss expects a trailing slash. +config.ASSETS_URL = staticfiles_storage.url('scss/assets/') + + +class DjangoScssCompiler(Compiler): + def __init__(self, **kwargs): + kwargs.setdefault('extensions', (DjangoExtension, CompassExtension)) + if not os.path.exists(config.ASSETS_ROOT): + os.makedirs(config.ASSETS_ROOT) + super(DjangoScssCompiler, self).__init__(**kwargs) + + def compile(self, *paths): + compilation = self.make_compilation() + for path in paths: + path = PurePath(path) + if path.is_absolute(): + path = path.relative_to('/') + filename, storage = get_file_and_storage(str(path)) + with storage.open(filename) as f: + source = SourceFile.from_file(f, origin=path.parent, relpath=PurePath(path.name)) + compilation.add_source(source) + return self.call_and_catch_errors(compilation.run) + + def compile_string(self, string, filename=None): + compilation = self.make_compilation() + if filename is not None: + f = StringIO(string) + filename = PurePath(filename) + source = SourceFile.from_file(f, origin=filename.parent, relpath=PurePath(filename.name)) + else: + source = SourceFile.from_string(string) + compilation.add_source(source) + return self.call_and_catch_errors(compilation.run) diff --git a/django_pyscss/compressor.py b/django_pyscss/compressor.py index 78d5fd0..68aec24 100644 --- a/django_pyscss/compressor.py +++ b/django_pyscss/compressor.py @@ -1,15 +1,13 @@ from __future__ import absolute_import -import os - from compressor.filters import FilterBase from compressor.conf import settings -from django_pyscss.scss import DjangoScss +from django_pyscss import DjangoScssCompiler class DjangoScssFilter(FilterBase): - compiler = DjangoScss() + compiler = DjangoScssCompiler() def __init__(self, content, attrs=None, filter_type=None, filename=None, **kwargs): # It looks like there is a bug in django-compressor because it expects @@ -21,10 +19,9 @@ class DjangoScssFilter(FilterBase): href = attrs['href'] except KeyError: # this is a style tag which means this is inline SCSS. - self.relative_to = None + self.filename = None else: - self.relative_to = os.path.dirname(href.replace(settings.STATIC_URL, '')) + self.filename = href.replace(settings.STATIC_URL, '') def input(self, **kwargs): - return self.compiler.compile(scss_string=self.content, - relative_to=self.relative_to) + return self.compiler.compile_string(self.content, filename=self.filename) diff --git a/django_pyscss/extension/__init__.py b/django_pyscss/extension/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/django_pyscss/extension/__init__.py diff --git a/django_pyscss/extension/django.py b/django_pyscss/extension/django.py new file mode 100644 index 0000000..c127270 --- /dev/null +++ b/django_pyscss/extension/django.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, unicode_literals + +from itertools import product +from pathlib import PurePath + +from scss.extension.core import CoreExtension +from scss.source import SourceFile + +from ..utils import get_file_and_storage + + +class DjangoExtension(CoreExtension): + name = 'django' + + def handle_import(self, name, compilation, rule): + """ + Re-implementation of the core Sass import mechanism, which looks for + files using the staticfiles storage and staticfiles finders. + """ + original_path = PurePath(name) + + if original_path.suffix: + search_exts = [original_path.suffix] + else: + search_exts = compilation.compiler.dynamic_extensions + + if original_path.is_absolute(): + # Remove the beginning slash + search_path = original_path.relative_to('/').parent + elif rule.source_file.origin: + search_path = rule.source_file.origin + else: + search_path = original_path.parent + + basename = original_path.stem + + for prefix, suffix in product(('_', ''), search_exts): + filename = PurePath(prefix + basename + suffix) + + full_filename, storage = get_file_and_storage(str(search_path / filename)) + + if full_filename: + with storage.open(full_filename) as f: + return SourceFile.from_file(f, origin=search_path, relpath=filename) diff --git a/django_pyscss/scss.py b/django_pyscss/scss.py deleted file mode 100644 index b4c5981..0000000 --- a/django_pyscss/scss.py +++ /dev/null @@ -1,223 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -import os -from itertools import product - -from django.contrib.staticfiles.storage import staticfiles_storage -from django.conf import settings - -from scss import ( - Scss, dequote, log, SourceFile, SassRule, config, -) - -from django_pyscss.utils import find_all_files - - -# TODO: It's really gross to modify this global settings variable. -# This is where PyScss is supposed to find the image files for making sprites. -config.STATIC_ROOT = find_all_files -config.STATIC_URL = staticfiles_storage.url('scss/') - -# This is where PyScss places the sprite files. -config.ASSETS_ROOT = os.path.join(settings.STATIC_ROOT, 'scss', 'assets') -# PyScss expects a trailing slash. -config.ASSETS_URL = staticfiles_storage.url('scss/assets/') - - -class DjangoScss(Scss): - """ - A subclass of the Scss compiler that uses the storages API for accessing - files. - """ - supported_extensions = ['.scss', '.sass', '.css'] - - def get_file_from_storage(self, filename): - try: - filename = staticfiles_storage.path(filename) - except NotImplementedError: - # remote storages don't implement path - pass - if staticfiles_storage.exists(filename): - return filename, staticfiles_storage - else: - return None, None - - def get_file_from_finders(self, filename): - for file_and_storage in find_all_files(filename): - return file_and_storage - return None, None - - def get_file_and_storage(self, filename): - # TODO: the switch probably shouldn't be on DEBUG - if settings.DEBUG: - return self.get_file_from_finders(filename) - else: - return self.get_file_from_storage(filename) - - def get_possible_import_paths(self, path, relative_to=None): - """ - Returns an iterable of possible paths for an import. - - relative_to is None in the case that the SCSS is being rendered from a - string or if it is the first file. - """ - paths = [] - - if path.startswith('/'): # absolute import - path = path[1:] - elif relative_to: # relative import - path = os.path.join(relative_to, path) - - dirname, filename = os.path.split(path) - name, ext = os.path.splitext(filename) - if ext: - search_exts = [ext] - else: - search_exts = self.supported_extensions - for prefix, suffix in product(('_', ''), search_exts): - paths.append(os.path.join(dirname, prefix + name + suffix)) - paths.append(path) - return paths - - def _find_source_file(self, filename, relative_to=None): - paths = self.get_possible_import_paths(filename, relative_to) - log.debug('Searching for %s in %s', filename, paths) - for name in paths: - full_filename, storage = self.get_file_and_storage(name) - if full_filename: - if full_filename not in self.source_file_index: - with storage.open(full_filename) as f: - source = f.read() - - source_file = SourceFile( - full_filename, - source, - ) - # SourceFile.__init__ calls os.path.realpath on this, we don't want - # that, we want them to remain relative. - source_file.parent_dir = os.path.dirname(name) - self.source_files.append(source_file) - self.source_file_index[full_filename] = source_file - return self.source_file_index[full_filename] - - def _do_import(self, rule, scope, block): - """ - Implements @import using the django storages API. - """ - # Protect against going to prohibited places... - if any(scary_token in block.argument for scary_token in ('..', '://', 'url(')): - rule.properties.append((block.prop, None)) - return - - full_filename = None - names = block.argument.split(',') - for name in names: - name = dequote(name.strip()) - - relative_to = rule.source_file.parent_dir - source_file = self._find_source_file(name, relative_to) - - if source_file is None: - i_codestr = self._do_magic_import(rule, scope, block) - - if i_codestr is not None: - source_file = SourceFile.from_string(i_codestr) - self.source_files.append(source_file) - self.source_file_index[full_filename] = source_file - - if source_file is None: - log.warn("File to import not found or unreadable: '%s' (%s)", name, rule.file_and_line) - continue - - import_key = (name, source_file.parent_dir) - if rule.namespace.has_import(import_key): - # If already imported in this scope, skip - continue - - _rule = SassRule( - source_file=source_file, - lineno=block.lineno, - import_key=import_key, - unparsed_contents=source_file.contents, - - # rule - options=rule.options, - properties=rule.properties, - extends_selectors=rule.extends_selectors, - ancestry=rule.ancestry, - namespace=rule.namespace, - ) - rule.namespace.add_import(import_key, rule.import_key, rule.file_and_line) - self.manage_children(_rule, scope) - - def Compilation(self, scss_string=None, scss_file=None, super_selector=None, - filename=None, is_sass=None, line_numbers=True, - relative_to=None): - """ - Overwritten to call _find_source_file instead of - SourceFile.from_filename. Also added the relative_to option. - """ - if not os.path.exists(config.ASSETS_ROOT): - os.makedirs(config.ASSETS_ROOT) - if super_selector: - self.super_selector = super_selector + ' ' - self.reset() - - source_file = None - if scss_string is not None: - source_file = SourceFile.from_string(scss_string, filename, is_sass, line_numbers) - # Set the parent_dir to be something meaningful instead of the - # current working directory, which is never correct for DjangoScss. - source_file.parent_dir = relative_to - elif scss_file is not None: - # Call _find_source_file instead of SourceFile.from_filename - source_file = self._find_source_file(scss_file) - - if source_file is not None: - # Clear the existing list of files - self.source_files = [] - self.source_file_index = dict() - - self.source_files.append(source_file) - self.source_file_index[source_file.filename] = source_file - - # this will compile and manage rule: child objects inside of a node - self.parse_children() - - # this will manage @extends - self.apply_extends() - - rules_by_file, css_files = self.parse_properties() - - all_rules = 0 - all_selectors = 0 - exceeded = '' - final_cont = '' - files = len(css_files) - for source_file in css_files: - rules = rules_by_file[source_file] - fcont, total_rules, total_selectors = self.create_css(rules) - all_rules += total_rules - all_selectors += total_selectors - if not exceeded and all_selectors > 4095: - exceeded = " (IE exceeded!)" - log.error("Maximum number of supported selectors in Internet Explorer (4095) exceeded!") - if files > 1 and self.scss_opts.get('debug_info', False): - if source_file.is_string: - final_cont += "/* %s %s generated add up to a total of %s %s accumulated%s */\n" % ( - total_selectors, - 'selector' if total_selectors == 1 else 'selectors', - all_selectors, - 'selector' if all_selectors == 1 else 'selectors', - exceeded) - else: - final_cont += "/* %s %s generated from '%s' add up to a total of %s %s accumulated%s */\n" % ( - total_selectors, - 'selector' if total_selectors == 1 else 'selectors', - source_file.filename, - all_selectors, - 'selector' if all_selectors == 1 else 'selectors', - exceeded) - final_cont += fcont - - return final_cont diff --git a/django_pyscss/utils.py b/django_pyscss/utils.py index d668280..1f9e4b4 100644 --- a/django_pyscss/utils.py +++ b/django_pyscss/utils.py @@ -1,7 +1,9 @@ import fnmatch import os +from django.conf import settings from django.contrib.staticfiles import finders +from django.contrib.staticfiles.storage import staticfiles_storage def find_all_files(glob): @@ -17,3 +19,29 @@ def find_all_files(glob): or '', path), glob): yield path, storage + + +def get_file_from_storage(filename): + try: + filename = staticfiles_storage.path(filename) + except NotImplementedError: + # remote storages don't implement path + pass + if staticfiles_storage.exists(filename): + return filename, staticfiles_storage + else: + return None, None + + +def get_file_from_finders(filename): + for file_and_storage in find_all_files(filename): + return file_and_storage + return None, None + + +def get_file_and_storage(filename): + # TODO: the switch probably shouldn't be on DEBUG + if settings.DEBUG: + return get_file_from_finders(filename) + else: + return get_file_from_storage(filename) |
