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