@@ -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 |
@@ -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 | |||
@@ -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): | |||
""" | |||
@@ -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) |
@@ -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): | |||
@@ -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): | |||
""" | |||
@@ -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; | |||
} |
@@ -0,0 +1,5 @@ | |||
.rounded (@radius: 5px){ | |||
-webkit-border-radius: @radius; | |||
-moz-border-radius: @radius; | |||
border-radius: @radius; | |||
} |
@@ -0,0 +1,5 @@ | |||
* { | |||
border: 0; | |||
padding: 0; | |||
margin: 0; | |||
} |
@@ -0,0 +1,2 @@ | |||
@the-border: 1px; | |||
@base-color: #111; |
@@ -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); | |||
} |
@@ -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 |
@@ -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(): | |||
@@ -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() |
@@ -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') |
@@ -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 | |||
@@ -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 | |||