Skip to content

Commit

Permalink
Merge branch 'refs/heads/stylus'
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrey Fedoseev committed Mar 7, 2015
2 parents a893a8b + 9f64c9c commit 573e973
Show file tree
Hide file tree
Showing 18 changed files with 279 additions and 7 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ before_install:
- npm install -g coffee-script
- npm install -g less@1.7.4
- npm install -g babel@4.3.0
- npm install -g stylus@0.50.0
- gem install sass --version 3.3.14
- gem install compass --version 1.0.1
install:
Expand Down
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
Changes
=======

Dev
===

- Add `Stylus <http://learnboost.github.io/stylus/>` compiler


0.9
===

Expand Down
16 changes: 16 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,11 @@ Compiles everything between ``{% inlinecompile %}`` and ``{% endinlinecompile %}
Compiler must be specified in ``STATIC_PRECOMPILER_COMPILERS`` setting. Names for default compilers are:

* ``coffeescript``
* ``babel``
* ``less``
* ``sass``
* ``scss``
* ``stylus``

Example Usage
-------------
Expand Down Expand Up @@ -109,6 +111,7 @@ General settings
'static_precompiler.compilers.SASS',
'static_precompiler.compilers.SCSS',
'static_precompiler.compilers.LESS',
'static_precompiler.compilers.Stylus',
)

You can specify compiler options using the following format::
Expand Down Expand Up @@ -207,6 +210,19 @@ Example::
)


Stylus
------

``executable``
Path to Stylus compiler executable. Default: ``"stylus"``.

Example::

STATIC_PRECOMPILER_COMPILERS = (
('static_precompiler.less.Stylus', {"executable": "/usr/bin/stylus"),
)


Usage with forms media
======================

Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def read(fname):
author_email="andrey.fedoseev@gmail.com",
url="https://github.com/andreyfedoseev/django-static-precompiler",
description="Django template tags to compile all kinds of static files "
"(SASS, LESS, CoffeeScript, Babel).",
"(SASS, LESS, Stylus, CoffeeScript, Babel).",
long_description="\n\n".join([README, CHANGES]),
classifiers=[
'Development Status :: 4 - Beta',
Expand All @@ -58,7 +58,7 @@ def read(fname):
'Programming Language :: Python :: 3',
'Topic :: Internet :: WWW/HTTP',
],
keywords=["sass", "scss", "less", "css", "coffeescript", "javascript", "babel"],
keywords=["sass", "scss", "less", "stylus", "css", "coffeescript", "javascript", "babel"],
tests_require=[
"pytest",
"pytest-django",
Expand Down
1 change: 1 addition & 0 deletions static_precompiler/compilers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from static_precompiler.compilers.babel import Babel
from static_precompiler.compilers.scss import SASS, SCSS
from static_precompiler.compilers.less import LESS
from static_precompiler.compilers.stylus import Stylus
136 changes: 136 additions & 0 deletions static_precompiler/compilers/stylus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from static_precompiler.utils import run_command, convert_urls
from static_precompiler.exceptions import StaticCompilationError
from static_precompiler.compilers.base import BaseCompiler
import os
import posixpath
import re


class Stylus(BaseCompiler):

name = "stylus"
input_extension = "styl"
output_extension = "css"

IMPORT_RE = re.compile(r"@(?:import|require)\s+(.+?)\s*$", re.MULTILINE)

def __init__(self, executable="stylus"):
self.executable = executable
super(Stylus, self).__init__()

def compile_source(self, source):
args = [
self.executable,
"-p",
]
out, errors = run_command(args, input=source)

if errors:
raise StaticCompilationError(errors)

return out

def compile_file(self, source_path):
full_source_path = self.get_full_source_path(source_path)
args = [
self.executable,
"-p",
full_source_path,
]

# `cwd` is a directory containing `source_path`. Ex: source_path = '1/2/3', full_source_path = '/abc/1/2/3' -> cwd = '/abc'
cwd = os.path.normpath(os.path.join(full_source_path, *([".."] * len(source_path.split("/")))))
out, errors = run_command(args, cwd=cwd)

if errors:
raise StaticCompilationError(errors)

return out

def postprocess(self, compiled, source_path):
return convert_urls(compiled, source_path)

def find_imports(self, source):
""" Find the imported files in the source code.
:param source: source code
:type source: str
:returns: list of str
"""
imports = set()
for import_string in self.IMPORT_RE.findall(source):
import_string = import_string.strip("'").strip('"').strip()
if not import_string:
continue
if import_string.startswith("url("):
continue
if import_string.endswith(".css"):
continue
if import_string.startswith("http://") or import_string.startswith("https://"):
continue
imports.add(import_string)
return sorted(imports)

def locate_imported_file(self, source_dir, import_path):
""" Locate the imported file in the source directory.
Return the path to the imported file relative to STATIC_ROOT
:param source_dir: source directory
:type source_dir: str
:param import_path: path to the imported file
:type import_path: str
:returns: str
"""
path = posixpath.normpath(posixpath.join(source_dir, import_path))

try:
self.get_full_source_path(path)
except ValueError:
raise StaticCompilationError(
"Can't locate the imported file: {0}".format(import_path)
)
return path

def find_dependencies(self, source_path):
source = self.get_source(source_path)
source_dir = posixpath.dirname(source_path)
dependencies = set()
imported_files = set()
for import_path in self.find_imports(source):
if import_path.endswith(".styl"):
# @import "foo.styl"
imported_files.add(self.locate_imported_file(source_dir, import_path))
elif import_path.endswith("/*"):
# @import "foo/*"
imported_dir = posixpath.join(source_dir, import_path[:-2])
try:
imported_dir_full_path = self.get_full_source_path(imported_dir)
except ValueError:
raise StaticCompilationError(
"Can't locate the imported directory: {0}".format(import_path)
)
if not os.path.isdir(imported_dir_full_path):
raise StaticCompilationError(
"Imported path is not a directory: {0}".format(import_path)
)
for filename in os.listdir(imported_dir_full_path):
if filename.endswith(".styl"):
imported_files.add(self.locate_imported_file(imported_dir, filename))
else:
try:
# @import "foo" -> @import "foo/index.styl"
imported_dir = posixpath.join(source_dir, import_path)
imported_dir_full_path = self.get_full_source_path(imported_dir)
if os.path.isdir(imported_dir_full_path):
imported_files.add(self.locate_imported_file(imported_dir, "index.styl"))
except ValueError:
# @import "foo" -> @import "foo.styl"
imported_files.add(self.locate_imported_file(source_dir, import_path + ".styl"))

dependencies.update(imported_files)
for imported_file in imported_files:
dependencies.update(self.find_dependencies(imported_file))

return sorted(dependencies)
1 change: 1 addition & 0 deletions static_precompiler/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"static_precompiler.compilers.SASS",
"static_precompiler.compilers.SCSS",
"static_precompiler.compilers.LESS",
"static_precompiler.compilers.Stylus",
))

ROOT = getattr(settings, "STATIC_PRECOMPILER_ROOT",
Expand Down
1 change: 1 addition & 0 deletions static_precompiler/tests/static/styles/stylus/A.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import 'B/*'
1 change: 1 addition & 0 deletions static_precompiler/tests/static/styles/stylus/B/C.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "../D.styl"
1 change: 1 addition & 0 deletions static_precompiler/tests/static/styles/stylus/D.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "E"
2 changes: 2 additions & 0 deletions static_precompiler/tests/static/styles/stylus/E/F.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
p
color: red;
1 change: 1 addition & 0 deletions static_precompiler/tests/static/styles/stylus/E/index.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "F"
1 change: 1 addition & 0 deletions static_precompiler/tests/static/styles/stylus/broken1.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "non-existing/*"
1 change: 1 addition & 0 deletions static_precompiler/tests/static/styles/stylus/broken2.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "not-a-directory/*"
1 change: 1 addition & 0 deletions static_precompiler/tests/static/styles/stylus/broken3.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "empty-directory"
Empty file.
15 changes: 10 additions & 5 deletions static_precompiler/tests/test_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ def test_compilestatic_command():
compiled_files.sort()

assert compiled_files == [
'another_test.js',
'scripts/test.js',
'styles/imported.css',
'styles/test.css',
'test-compass.css',
"another_test.js",
"scripts/test.js",
"styles/imported.css",
"styles/stylus/A.css",
"styles/stylus/B/C.css",
"styles/stylus/D.css",
"styles/stylus/E/F.css",
"styles/stylus/E/index.css",
"styles/test.css",
"test-compass.css",
]
97 changes: 97 additions & 0 deletions static_precompiler/tests/test_stylus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# coding: utf-8
from pretend import call_recorder, call
from static_precompiler.compilers import Stylus
from static_precompiler.exceptions import StaticCompilationError
from static_precompiler.utils import normalize_path, fix_line_breaks
import os
import pytest


def test_compile_file():
compiler = Stylus()

assert fix_line_breaks(compiler.compile_file("styles/stylus/A.styl")) == "p {\n color: #f00;\n}\n"

with pytest.raises(StaticCompilationError):
assert compiler.compile_file("styles/stylus/broken1.styl")


def test_compile_source():
compiler = Stylus()

assert fix_line_breaks(compiler.compile_source("p\n color: red;")) == "p {\n color: #f00;\n}\n\n"

with pytest.raises(StaticCompilationError):
assert compiler.compile_source("broken")


def test_postprocesss(monkeypatch):
compiler = Stylus()
convert_urls = call_recorder(lambda *args: "spam")
monkeypatch.setattr("static_precompiler.compilers.stylus.convert_urls", convert_urls)
assert compiler.postprocess("ham", "eggs") == "spam"
assert convert_urls.calls == [call("ham", "eggs")]


def test_find_imports():
source = """
@import " "
@import "foo.styl"
@import 'foo'
@import "foo.css"
@import "http://foo.com/bar"
@import "https://foo.com/bar"
@import url(foo)
@import url(http://fonts.googleapis.com/css?family=Arvo:400,700,400italic,700italic)
@import url("http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,400,700,600,300")
@require "foo.styl"
@require "foo/*"
"""

expected = [
"foo",
"foo.styl",
"foo/*",
]

compiler = Stylus()
assert compiler.find_imports(source) == expected


def test_locate_imported_file(monkeypatch):
compiler = Stylus()

root = os.path.dirname(__file__)

existing_files = set()
for f in ("A/B.styl", "C.styl"):
existing_files.add(os.path.join(root, "static", normalize_path(f)))

monkeypatch.setattr("os.path.exists", lambda x: x in existing_files)

assert compiler.locate_imported_file("A", "B.styl") == "A/B.styl"
assert compiler.locate_imported_file("", "C.styl") == "C.styl"

with pytest.raises(StaticCompilationError):
compiler.locate_imported_file("", "Z.styl")


def test_find_dependencies():

compiler = Stylus()

assert compiler.find_dependencies("styles/stylus/A.styl") == [
"styles/stylus/B/C.styl",
"styles/stylus/D.styl",
"styles/stylus/E/F.styl",
"styles/stylus/E/index.styl",
]

with pytest.raises(StaticCompilationError):
compiler.find_dependencies("styles/stylus/broken1.styl")

with pytest.raises(StaticCompilationError):
compiler.find_dependencies("styles/stylus/broken2.styl")

with pytest.raises(StaticCompilationError):
compiler.find_dependencies("styles/stylus/broken3.styl")

0 comments on commit 573e973

Please sign in to comment.