summaryrefslogtreecommitdiff
path: root/django_pyscss
diff options
context:
space:
mode:
Diffstat (limited to 'django_pyscss')
-rw-r--r--django_pyscss/__init__.py1
-rw-r--r--django_pyscss/compiler.py58
-rw-r--r--django_pyscss/compressor.py13
-rw-r--r--django_pyscss/extension/__init__.py0
-rw-r--r--django_pyscss/extension/django.py44
-rw-r--r--django_pyscss/scss.py223
-rw-r--r--django_pyscss/utils.py28
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)