@@ -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) | jinja2_filters.register(self.env) | ||||
@property | |||||
def exception_class(self): | |||||
return TemplateError | |||||
def render(self, text, context): | def render(self, text, context): | ||||
""" | """ | ||||
Renders the given resource using the context | Renders the given resource using the context | ||||
@@ -184,6 +184,18 @@ class File(FS): | |||||
break | break | ||||
return False | 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 | @property | ||||
def is_text(self): | def is_text(self): | ||||
"""Return true if this is a text file.""" | """Return true if this is a text file.""" | ||||
@@ -222,6 +234,13 @@ class File(FS): | |||||
shutil.copy(self.path, str(destination)) | shutil.copy(self.path, str(destination)) | ||||
return target | return target | ||||
def delete(self): | |||||
""" | |||||
Delete the file if it exists. | |||||
""" | |||||
if self.exists: | |||||
os.remove(self.path) | |||||
class FSVisitor(object): | class FSVisitor(object): | ||||
""" | """ | ||||
@@ -39,12 +39,20 @@ class Generator(object): | |||||
def __getattr__(self, method_name): | def __getattr__(self, method_name): | ||||
if hasattr(Plugin, method_name): | if hasattr(Plugin, method_name): | ||||
def __call_plugins__(*args, **kwargs): | |||||
def __call_plugins__(*args): | |||||
res = None | |||||
if self.site.plugins: | if self.site.plugins: | ||||
for plugin in self.site.plugins: | for plugin in self.site.plugins: | ||||
if hasattr(plugin, method_name): | if hasattr(plugin, method_name): | ||||
function = getattr(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__ | return __call_plugins__ | ||||
raise HydeException( | raise HydeException( | ||||
"Unknown plugin method [%s] called." % method_name) | "Unknown plugin method [%s] called." % method_name) | ||||
@@ -186,12 +194,11 @@ class Generator(object): | |||||
self.events.node_complete(node) | self.events.node_complete(node) | ||||
def __generate_resource__(self, resource): | def __generate_resource__(self, resource): | ||||
if not resource.is_processable: | |||||
logger.info("Skipping [%s]", resource) | |||||
return | |||||
logger.info("Processing [%s]", resource) | logger.info("Processing [%s]", resource) | ||||
with self.context_for_resource(resource) as context: | 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: | if resource.source_file.is_text: | ||||
text = resource.source_file.read_all() | text = resource.source_file.read_all() | ||||
text = self.events.begin_text_resource(resource, text) or text | 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.template.render(text, context) | ||||
text = self.events.text_resource_complete( | text = self.events.text_resource_complete( | ||||
resource, text) or text | resource, text) or text | ||||
target = File(self.site.config.deploy_root_path.child( | |||||
resource.relative_deploy_path)) | |||||
target.parent.make() | |||||
target.write(text) | target.write(text) | ||||
else: | else: | ||||
logger.info("Copying binary file [%s]", resource) | logger.info("Copying binary file [%s]", resource) | ||||
self.events.begin_binary_resource(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) | resource.source_file.copy_to(target) | ||||
self.events.binary_resource_complete(resource) | self.events.binary_resource_complete(resource) |
@@ -21,6 +21,7 @@ class Processable(object): | |||||
def __init__(self, source): | def __init__(self, source): | ||||
super(Processable, self).__init__() | super(Processable, self).__init__() | ||||
self.source = FS.file_or_folder(source) | self.source = FS.file_or_folder(source) | ||||
self.is_processable = True | |||||
@property | @property | ||||
def name(self): | def name(self): | ||||
@@ -54,6 +55,8 @@ class Resource(Processable): | |||||
raise HydeException("Source file is required" | raise HydeException("Source file is required" | ||||
" to instantiate a resource") | " to instantiate a resource") | ||||
self.node = node | self.node = node | ||||
self.site = node.site | |||||
self._relative_deploy_path = None | |||||
@property | @property | ||||
def relative_path(self): | def relative_path(self): | ||||
@@ -62,6 +65,23 @@ class Resource(Processable): | |||||
""" | """ | ||||
return self.source_file.get_relative_path(self.node.root.source_folder) | 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): | class Node(Processable): | ||||
""" | """ | ||||
@@ -100,7 +120,8 @@ class Node(Processable): | |||||
""" | """ | ||||
if self.contains_resource(resource_name): | 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 | return None | ||||
def add_child_node(self, folder): | def add_child_node(self, folder): | ||||
@@ -3,7 +3,7 @@ | |||||
""" | """ | ||||
Abstract classes and utilities for template engines | Abstract classes and utilities for template engines | ||||
""" | """ | ||||
from hyde.exceptions import HydeException | |||||
class Template(object): | class Template(object): | ||||
""" | """ | ||||
@@ -31,6 +31,13 @@ class Template(object): | |||||
abstract | abstract | ||||
@property | |||||
def exception_class(self): | |||||
return HydeException | |||||
def get_include_statement(self, path_to_include): | |||||
return "{%% include '%s' %%}" % path_to_include | |||||
@staticmethod | @staticmethod | ||||
def find_template(site): | 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(): | def test_extension(): | ||||
f = File(__file__) | f = File(__file__) | ||||
assert f.extension == os.path.splitext(__file__)[1] | assert f.extension == os.path.splitext(__file__)[1] | ||||
f = File("abc") | |||||
assert f.extension == '' | |||||
def test_kind(): | def test_kind(): | ||||
f = File(__file__) | f = File(__file__) | ||||
assert f.kind == os.path.splitext(__file__)[1].lstrip('.') | 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(): | def test_path_expands_user(): | ||||
f = File("~/abc/def") | f = File("~/abc/def") | ||||
@@ -96,6 +108,21 @@ def test_remove_folder(): | |||||
c.delete() | c.delete() | ||||
assert not c.exists | 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(): | def test_file_or_folder(): | ||||
f = FS.file_or_folder(__file__) | f = FS.file_or_folder(__file__) | ||||
assert isinstance(f, File) | assert isinstance(f, File) | ||||
@@ -131,11 +158,8 @@ def test_ancestors_stop(): | |||||
def test_is_descendant_of(): | def test_is_descendant_of(): | ||||
assert INDEX.is_descendant_of(JINJA2) | assert INDEX.is_descendant_of(JINJA2) | ||||
print "*" | |||||
assert JINJA2.is_descendant_of(TEMPLATE_ROOT) | assert JINJA2.is_descendant_of(TEMPLATE_ROOT) | ||||
print "*" | |||||
assert INDEX.is_descendant_of(TEMPLATE_ROOT) | assert INDEX.is_descendant_of(TEMPLATE_ROOT) | ||||
print "*" | |||||
assert not INDEX.is_descendant_of(DATA_ROOT) | assert not INDEX.is_descendant_of(DATA_ROOT) | ||||
def test_get_relative_path(): | def test_get_relative_path(): | ||||
@@ -35,3 +35,28 @@ def test_generate_resource_from_path(): | |||||
text = about.read_all() | text = about.read_all() | ||||
q = PyQuery(text) | q = PyQuery(text) | ||||
assert about.name in q("div#main").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.content_root_path == TEST_SITE_ROOT.child_folder('site/stuff') | ||||
assert c.media_root_path == TEST_SITE_ROOT.child_folder('mmm') | assert c.media_root_path == TEST_SITE_ROOT.child_folder('mmm') | ||||
assert c.media_url == TEST_SITE_ROOT.child_folder('/media') | assert c.media_url == TEST_SITE_ROOT.child_folder('/media') | ||||
print c.deploy_root_path | |||||
assert c.deploy_root_path == Folder('~/deploy_site') | assert c.deploy_root_path == Folder('~/deploy_site') |
@@ -88,7 +88,7 @@ def test_contains_resource(): | |||||
path = 'blog/2010/december' | path = 'blog/2010/december' | ||||
node = s.content.node_from_relative_path(path) | node = s.content.node_from_relative_path(path) | ||||
assert node.contains_resource('merry-christmas.html') | assert node.contains_resource('merry-christmas.html') | ||||
def test_get_resource(): | def test_get_resource(): | ||||
s = Site(TEST_SITE_ROOT) | s = Site(TEST_SITE_ROOT) | ||||
s.load() | s.load() | ||||
@@ -97,6 +97,29 @@ def test_get_resource(): | |||||
resource = node.get_resource('merry-christmas.html') | resource = node.get_resource('merry-christmas.html') | ||||
assert resource == s.content.resource_from_relative_path(Folder(path).child('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): | class TestSiteWithConfig(object): | ||||
@classmethod | @classmethod | ||||
@@ -12,7 +12,7 @@ def assert_html_equals(expected, actual, sanitize=None): | |||||
expected = sanitize(expected) | expected = sanitize(expected) | ||||
actual = sanitize(actual) | actual = sanitize(actual) | ||||
assert expected == actual | assert expected == actual | ||||
def trap_exit_fail(f): | def trap_exit_fail(f): | ||||
def test_wrapper(*args): | def test_wrapper(*args): | ||||
try: | try: | ||||
@@ -25,7 +25,6 @@ def trap_exit_fail(f): | |||||
def trap_exit_pass(f): | def trap_exit_pass(f): | ||||
def test_wrapper(*args): | def test_wrapper(*args): | ||||
try: | try: | ||||
print f.__name__ | |||||
f(*args) | f(*args) | ||||
except SystemExit: | except SystemExit: | ||||
pass | pass | ||||