Browse Source

Added less support.

main
Lakshmi Vyasarajan 14 years ago
parent
commit
3ff724f2ae
17 changed files with 321 additions and 15 deletions
  1. +84
    -0
      hyde/ext/plugins/less.py
  2. +4
    -0
      hyde/ext/templates/jinja.py
  3. +19
    -0
      hyde/fs.py
  4. +18
    -6
      hyde/generator.py
  5. +22
    -1
      hyde/site.py
  6. +8
    -1
      hyde/template.py
  7. +18
    -0
      hyde/tests/ext/less/expected-site.css
  8. +5
    -0
      hyde/tests/ext/less/inc/mixin.less
  9. +5
    -0
      hyde/tests/ext/less/inc/reset.css
  10. +2
    -0
      hyde/tests/ext/less/inc/vars.less
  11. +17
    -0
      hyde/tests/ext/less/site.less
  12. +42
    -0
      hyde/tests/ext/test_less.py
  13. +27
    -3
      hyde/tests/test_fs.py
  14. +25
    -0
      hyde/tests/test_generate.py
  15. +0
    -1
      hyde/tests/test_model.py
  16. +24
    -1
      hyde/tests/test_site.py
  17. +1
    -2
      hyde/tests/util.py

+ 84
- 0
hyde/ext/plugins/less.py View File

@@ -0,0 +1,84 @@
"""
Less css plugin
"""

from hyde.plugin import Plugin
from hyde.fs import File, Folder

import re
import subprocess


class LessCSSPlugin(Plugin):
"""
The plugin class for less css
"""

def __init__(self, site):
super(LessCSSPlugin, self).__init__(site)

def template_loaded(self, template):
self.template = template

def begin_text_resource(self, resource, text):
"""
Replace @import statements with {% include %} statements.
"""

if not resource.source_file.kind == 'less':
return
import_finder = re.compile(
'^\\s*@import\s+(?:\'|\")([^\'\"]*)(?:\'|\")\s*\;\s*$',
re.MULTILINE)

def import_to_include(match):
if not match.lastindex:
return ''
path = match.groups(1)[0]
afile = File(resource.source_file.parent.child(path))
if len(afile.kind.strip()) == 0:
afile = File(afile.path + '.less')
ref = self.site.content.resource_from_path(afile.path)
if not ref:
raise self.template.exception_class(
"Cannot import from path [%s]" % afile.path)
ref.is_processable = False
return self.template.get_include_statement(ref.relative_path)
text = import_finder.sub(import_to_include, text)
return text

def text_resource_complete(self, resource, text):
"""
Save the file to a temporary place and run less compiler.
Read the generated file and return the text as output.
Set the target path to have a css extension.
"""
if not resource.source_file.kind == 'less':
return
if not (hasattr(self.site.config, 'less') and
hasattr(self.site.config.less, 'app')):
raise self.template.exception_class(
"Less css path not configured. "
"This plugin expects `less.app` to point "
"to the `lessc` executable.")

less = File(self.site.config.less.app)
if not File(less).exists:
raise self.template.exception_class(
"Cannot find the less executable. The given path [%s] "
"is incorrect" % less)

source = File.make_temp(text)
target = File.make_temp('')
try:
subprocess.check_call([str(less), str(source), str(target)])
except subprocess.CalledProcessError:
raise self.template.exception_class(
"Cannot process less css. Error occurred when "
"processing [%s]" % resource.source_file)

out = target.read_all()
new_name = resource.source_file.name_without_extension + ".css"
target_folder = File(resource.relative_path).parent
resource.relative_deploy_path = target_folder.child(new_name)
return out

+ 4
- 0
hyde/ext/templates/jinja.py View File

@@ -93,6 +93,10 @@ class Jinja2Template(Template):
jinja2_filters.register(self.env)


@property
def exception_class(self):
return TemplateError

def render(self, text, context):
"""
Renders the given resource using the context


+ 19
- 0
hyde/fs.py View File

@@ -184,6 +184,18 @@ class File(FS):
break
return False

@staticmethod
def make_temp(text):
"""
Creates a temprorary file and writes the `text` into it
"""
import tempfile
(handle, path) = tempfile.mkstemp(text=True)
os.close(handle)
f = File(path)
f.write(text)
return f

@property
def is_text(self):
"""Return true if this is a text file."""
@@ -222,6 +234,13 @@ class File(FS):
shutil.copy(self.path, str(destination))
return target

def delete(self):
"""
Delete the file if it exists.
"""
if self.exists:
os.remove(self.path)


class FSVisitor(object):
"""


+ 18
- 6
hyde/generator.py View File

@@ -39,12 +39,20 @@ class Generator(object):
def __getattr__(self, method_name):
if hasattr(Plugin, method_name):

def __call_plugins__(*args, **kwargs):
def __call_plugins__(*args):
res = None
if self.site.plugins:
for plugin in self.site.plugins:
if hasattr(plugin, method_name):
function = getattr(plugin, method_name)
function(*args, **kwargs)
res = function(*args)
if res:
targs = list(args)
last = targs.pop
targs.append(res if res else last)
args = tuple(targs)
return res

return __call_plugins__
raise HydeException(
"Unknown plugin method [%s] called." % method_name)
@@ -186,12 +194,11 @@ class Generator(object):
self.events.node_complete(node)

def __generate_resource__(self, resource):
if not resource.is_processable:
logger.info("Skipping [%s]", resource)
return
logger.info("Processing [%s]", resource)
with self.context_for_resource(resource) as context:
target = File(resource.source_file.get_mirror(
self.site.config.deploy_root_path,
self.site.content.source_folder))
target.parent.make()
if resource.source_file.is_text:
text = resource.source_file.read_all()
text = self.events.begin_text_resource(resource, text) or text
@@ -199,9 +206,14 @@ class Generator(object):
text = self.template.render(text, context)
text = self.events.text_resource_complete(
resource, text) or text
target = File(self.site.config.deploy_root_path.child(
resource.relative_deploy_path))
target.parent.make()
target.write(text)
else:
logger.info("Copying binary file [%s]", resource)
self.events.begin_binary_resource(resource)
target = File(self.site.config.deploy_root_path.child(
resource.relative_deploy_path))
resource.source_file.copy_to(target)
self.events.binary_resource_complete(resource)

+ 22
- 1
hyde/site.py View File

@@ -21,6 +21,7 @@ class Processable(object):
def __init__(self, source):
super(Processable, self).__init__()
self.source = FS.file_or_folder(source)
self.is_processable = True

@property
def name(self):
@@ -54,6 +55,8 @@ class Resource(Processable):
raise HydeException("Source file is required"
" to instantiate a resource")
self.node = node
self.site = node.site
self._relative_deploy_path = None

@property
def relative_path(self):
@@ -62,6 +65,23 @@ class Resource(Processable):
"""
return self.source_file.get_relative_path(self.node.root.source_folder)

def get_relative_deploy_path(self):
"""
Gets the path where the file will be created
after its been processed.
"""
return self._relative_deploy_path \
if self._relative_deploy_path \
else self.relative_path

def set_relative_deploy_path(self, path):
"""
Sets the path where the file ought to be created
after its been processed.
"""
self._relative_deploy_path = path

relative_deploy_path = property(get_relative_deploy_path, set_relative_deploy_path)

class Node(Processable):
"""
@@ -100,7 +120,8 @@ class Node(Processable):
"""

if self.contains_resource(resource_name):
return self.root.resource_from_path(self.source_folder.child(resource_name))
return self.root.resource_from_path(
self.source_folder.child(resource_name))
return None

def add_child_node(self, folder):


+ 8
- 1
hyde/template.py View File

@@ -3,7 +3,7 @@
"""
Abstract classes and utilities for template engines
"""
from hyde.exceptions import HydeException

class Template(object):
"""
@@ -31,6 +31,13 @@ class Template(object):

abstract

@property
def exception_class(self):
return HydeException

def get_include_statement(self, path_to_include):
return "{%% include '%s' %%}" % path_to_include

@staticmethod
def find_template(site):
"""


+ 18
- 0
hyde/tests/ext/less/expected-site.css View File

@@ -0,0 +1,18 @@
* {
border: 0;
padding: 0;
margin: 0;
}
#header {
color: #333333;
border-left: 1px;
border-right: 2px;
}
#footer {
color: #333333;
}
#content {
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
}

+ 5
- 0
hyde/tests/ext/less/inc/mixin.less View File

@@ -0,0 +1,5 @@
.rounded (@radius: 5px){
-webkit-border-radius: @radius;
-moz-border-radius: @radius;
border-radius: @radius;
}

+ 5
- 0
hyde/tests/ext/less/inc/reset.css View File

@@ -0,0 +1,5 @@
* {
border: 0;
padding: 0;
margin: 0;
}

+ 2
- 0
hyde/tests/ext/less/inc/vars.less View File

@@ -0,0 +1,2 @@
@the-border: 1px;
@base-color: #111;

+ 17
- 0
hyde/tests/ext/less/site.less View File

@@ -0,0 +1,17 @@
@import "inc/mixin";
@import "inc/vars";
@import "inc/reset.css";

#header {
color: @base-color * 3;
border-left: @the-border;
border-right: @the-border * 2;
}

#footer {
color: (@base-color + #111) * 1.5;
}

#content {
.rounded(10px);
}

+ 42
- 0
hyde/tests/ext/test_less.py View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
"""
Use nose
`$ pip install nose`
`$ nosetests`
"""
from hyde.fs import File, Folder
from hyde.model import Expando
from hyde.generator import Generator
from hyde.site import Site

LESS_SOURCE = File(__file__).parent.child_folder('less')
TEST_SITE = File(__file__).parent.parent.child_folder('_test')


class TestLess(object):

def setUp(self):
TEST_SITE.make()
TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
LESS_SOURCE.copy_contents_to(TEST_SITE.child('content/media/css'))
File(TEST_SITE.child('content/media/css/site.css')).delete()


def tearDown(self):
TEST_SITE.delete()

def test_can_execute_less(self):
s = Site(TEST_SITE)
s.config.plugins = ['hyde.ext.plugins.less.LessCSSPlugin']
s.config.less = Expando(dict(app='/Users/lvfp/local/bin/lessc'))
source = TEST_SITE.child('content/media/css/site.less')
target = File(Folder(s.config.deploy_root_path).child('media/css/site.css'))
gen = Generator(s)
gen.generate_resource_at_path(source)

assert target.exists
text = target.read_all()
expected_text = File(LESS_SOURCE.child('expected-site.css')).read_all()

assert text == expected_text

+ 27
- 3
hyde/tests/test_fs.py View File

@@ -39,10 +39,22 @@ def test_name_without_extension():
def test_extension():
f = File(__file__)
assert f.extension == os.path.splitext(__file__)[1]
f = File("abc")
assert f.extension == ''

def test_kind():
f = File(__file__)
assert f.kind == os.path.splitext(__file__)[1].lstrip('.')
f = File("abc")
assert f.kind == ''

def test_can_create_temp_file():
text = "A for apple"
f = File.make_temp(text)
assert f.exists
assert text == f.read_all()
f.delete()
assert not f.exists

def test_path_expands_user():
f = File("~/abc/def")
@@ -96,6 +108,21 @@ def test_remove_folder():
c.delete()
assert not c.exists

def test_can_remove_file():
f = FS(__file__).parent
c = f.child_folder('__test__')
c.make()
assert c.exists
txt = "abc"
abc = File(c.child('abc.txt'))
abc.write(txt)
assert abc.exists
abc.delete()
assert not abc.exists
abc.delete()
assert True # No Exception
c.delete()

def test_file_or_folder():
f = FS.file_or_folder(__file__)
assert isinstance(f, File)
@@ -131,11 +158,8 @@ def test_ancestors_stop():

def test_is_descendant_of():
assert INDEX.is_descendant_of(JINJA2)
print "*"
assert JINJA2.is_descendant_of(TEMPLATE_ROOT)
print "*"
assert INDEX.is_descendant_of(TEMPLATE_ROOT)
print "*"
assert not INDEX.is_descendant_of(DATA_ROOT)

def test_get_relative_path():


+ 25
- 0
hyde/tests/test_generate.py View File

@@ -35,3 +35,28 @@ def test_generate_resource_from_path():
text = about.read_all()
q = PyQuery(text)
assert about.name in q("div#main").text()

@with_setup(create_test_site, delete_test_site)
def test_generate_resource_from_path_with_is_processable_false():
site = Site(TEST_SITE)
site.load()
resource = site.content.resource_from_path(TEST_SITE.child('content/about.html'))
resource.is_processable = False
gen = Generator(site)
gen.generate_resource_at_path(TEST_SITE.child('content/about.html'))
about = File(Folder(site.config.deploy_root_path).child('about.html'))
assert not about.exists

@with_setup(create_test_site, delete_test_site)
def test_generate_resource_from_path_with_deploy_override():
site = Site(TEST_SITE)
site.load()
resource = site.content.resource_from_path(TEST_SITE.child('content/about.html'))
resource.relative_deploy_path = 'about/index.html'
gen = Generator(site)
gen.generate_resource_at_path(TEST_SITE.child('content/about.html'))
about = File(Folder(site.config.deploy_root_path).child('about/index.html'))
assert about.exists
text = about.read_all()
q = PyQuery(text)
assert resource.name in q("div#main").text()

+ 0
- 1
hyde/tests/test_model.py View File

@@ -87,5 +87,4 @@ class TestConfig(object):
assert c.content_root_path == TEST_SITE_ROOT.child_folder('site/stuff')
assert c.media_root_path == TEST_SITE_ROOT.child_folder('mmm')
assert c.media_url == TEST_SITE_ROOT.child_folder('/media')
print c.deploy_root_path
assert c.deploy_root_path == Folder('~/deploy_site')

+ 24
- 1
hyde/tests/test_site.py View File

@@ -88,7 +88,7 @@ def test_contains_resource():
path = 'blog/2010/december'
node = s.content.node_from_relative_path(path)
assert node.contains_resource('merry-christmas.html')
def test_get_resource():
s = Site(TEST_SITE_ROOT)
s.load()
@@ -97,6 +97,29 @@ def test_get_resource():
resource = node.get_resource('merry-christmas.html')
assert resource == s.content.resource_from_relative_path(Folder(path).child('merry-christmas.html'))

def test_is_processable_default_true():
s = Site(TEST_SITE_ROOT)
s.load()
for page in s.content.walk_resources():
assert page.is_processable

def test_relative_deploy_path():
s = Site(TEST_SITE_ROOT)
s.load()
for page in s.content.walk_resources():
assert page.relative_deploy_path == Folder(page.relative_path)

def test_relative_deploy_path_override():
s = Site(TEST_SITE_ROOT)
s.load()
res = s.content.resource_from_relative_path('blog/2010/december/merry-christmas.html')
res.relative_deploy_path = 'blog/2010/december/happy-holidays.html'
for page in s.content.walk_resources():
if res.source_file == page.source_file:
assert page.relative_deploy_path == 'blog/2010/december/happy-holidays.html'
else:
assert page.relative_deploy_path == Folder(page.relative_path)

class TestSiteWithConfig(object):

@classmethod


+ 1
- 2
hyde/tests/util.py View File

@@ -12,7 +12,7 @@ def assert_html_equals(expected, actual, sanitize=None):
expected = sanitize(expected)
actual = sanitize(actual)
assert expected == actual
def trap_exit_fail(f):
def test_wrapper(*args):
try:
@@ -25,7 +25,6 @@ def trap_exit_fail(f):
def trap_exit_pass(f):
def test_wrapper(*args):
try:
print f.__name__
f(*args)
except SystemExit:
pass


Loading…
Cancel
Save