From 3ff724f2aeb12c119f483ec89facff2cbb222040 Mon Sep 17 00:00:00 2001 From: Lakshmi Vyasarajan Date: Wed, 5 Jan 2011 01:59:20 +0530 Subject: [PATCH] Added less support. --- hyde/ext/plugins/less.py | 84 +++++++++++++++++++++++++++ hyde/ext/templates/jinja.py | 4 ++ hyde/fs.py | 19 ++++++ hyde/generator.py | 24 ++++++-- hyde/site.py | 23 +++++++- hyde/template.py | 9 ++- hyde/tests/ext/less/expected-site.css | 18 ++++++ hyde/tests/ext/less/inc/mixin.less | 5 ++ hyde/tests/ext/less/inc/reset.css | 5 ++ hyde/tests/ext/less/inc/vars.less | 2 + hyde/tests/ext/less/site.less | 17 ++++++ hyde/tests/ext/test_less.py | 42 ++++++++++++++ hyde/tests/test_fs.py | 30 +++++++++- hyde/tests/test_generate.py | 25 ++++++++ hyde/tests/test_model.py | 1 - hyde/tests/test_site.py | 25 +++++++- hyde/tests/util.py | 3 +- 17 files changed, 321 insertions(+), 15 deletions(-) create mode 100644 hyde/ext/plugins/less.py create mode 100644 hyde/tests/ext/less/expected-site.css create mode 100644 hyde/tests/ext/less/inc/mixin.less create mode 100644 hyde/tests/ext/less/inc/reset.css create mode 100644 hyde/tests/ext/less/inc/vars.less create mode 100644 hyde/tests/ext/less/site.less create mode 100644 hyde/tests/ext/test_less.py diff --git a/hyde/ext/plugins/less.py b/hyde/ext/plugins/less.py new file mode 100644 index 0000000..94fd9c2 --- /dev/null +++ b/hyde/ext/plugins/less.py @@ -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 diff --git a/hyde/ext/templates/jinja.py b/hyde/ext/templates/jinja.py index 16e83d1..2c38b57 100644 --- a/hyde/ext/templates/jinja.py +++ b/hyde/ext/templates/jinja.py @@ -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 diff --git a/hyde/fs.py b/hyde/fs.py index d3cf4b0..04730ef 100644 --- a/hyde/fs.py +++ b/hyde/fs.py @@ -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): """ diff --git a/hyde/generator.py b/hyde/generator.py index e7c4c39..fc7fe13 100644 --- a/hyde/generator.py +++ b/hyde/generator.py @@ -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) diff --git a/hyde/site.py b/hyde/site.py index 26b2465..a9ff8cc 100644 --- a/hyde/site.py +++ b/hyde/site.py @@ -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): diff --git a/hyde/template.py b/hyde/template.py index 1a382ff..42500f9 100644 --- a/hyde/template.py +++ b/hyde/template.py @@ -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): """ diff --git a/hyde/tests/ext/less/expected-site.css b/hyde/tests/ext/less/expected-site.css new file mode 100644 index 0000000..63edbe8 --- /dev/null +++ b/hyde/tests/ext/less/expected-site.css @@ -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; +} diff --git a/hyde/tests/ext/less/inc/mixin.less b/hyde/tests/ext/less/inc/mixin.less new file mode 100644 index 0000000..65643ec --- /dev/null +++ b/hyde/tests/ext/less/inc/mixin.less @@ -0,0 +1,5 @@ +.rounded (@radius: 5px){ + -webkit-border-radius: @radius; + -moz-border-radius: @radius; + border-radius: @radius; +} \ No newline at end of file diff --git a/hyde/tests/ext/less/inc/reset.css b/hyde/tests/ext/less/inc/reset.css new file mode 100644 index 0000000..6f4f9ef --- /dev/null +++ b/hyde/tests/ext/less/inc/reset.css @@ -0,0 +1,5 @@ +* { + border: 0; + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/hyde/tests/ext/less/inc/vars.less b/hyde/tests/ext/less/inc/vars.less new file mode 100644 index 0000000..85c9b2e --- /dev/null +++ b/hyde/tests/ext/less/inc/vars.less @@ -0,0 +1,2 @@ +@the-border: 1px; +@base-color: #111; \ No newline at end of file diff --git a/hyde/tests/ext/less/site.less b/hyde/tests/ext/less/site.less new file mode 100644 index 0000000..f6e340c --- /dev/null +++ b/hyde/tests/ext/less/site.less @@ -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); +} \ No newline at end of file diff --git a/hyde/tests/ext/test_less.py b/hyde/tests/ext/test_less.py new file mode 100644 index 0000000..011187b --- /dev/null +++ b/hyde/tests/ext/test_less.py @@ -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 \ No newline at end of file diff --git a/hyde/tests/test_fs.py b/hyde/tests/test_fs.py index aa85424..e7716d6 100644 --- a/hyde/tests/test_fs.py +++ b/hyde/tests/test_fs.py @@ -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(): diff --git a/hyde/tests/test_generate.py b/hyde/tests/test_generate.py index db87a20..81266c1 100644 --- a/hyde/tests/test_generate.py +++ b/hyde/tests/test_generate.py @@ -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() \ No newline at end of file diff --git a/hyde/tests/test_model.py b/hyde/tests/test_model.py index 865d99f..6c9c2fc 100644 --- a/hyde/tests/test_model.py +++ b/hyde/tests/test_model.py @@ -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') \ No newline at end of file diff --git a/hyde/tests/test_site.py b/hyde/tests/test_site.py index 499c9ed..4118681 100644 --- a/hyde/tests/test_site.py +++ b/hyde/tests/test_site.py @@ -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 diff --git a/hyde/tests/util.py b/hyde/tests/util.py index 08677cd..d7d14b5 100644 --- a/hyde/tests/util.py +++ b/hyde/tests/util.py @@ -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