| @@ -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 | |||