| @@ -70,36 +70,13 @@ class Engine(Application): | |||||
| The generate command. Generates the site at the given deployment directory. | The generate command. Generates the site at the given deployment directory. | ||||
| """ | """ | ||||
| sitepath = Folder(args.sitepath) | sitepath = Folder(args.sitepath) | ||||
| logger.info("Generating site at [%s]" % sitepath) | |||||
| # Read the configuration | |||||
| config_file = sitepath.child(args.config) | config_file = sitepath.child(args.config) | ||||
| logger.info("Reading site configuration from [%s]", config_file) | logger.info("Reading site configuration from [%s]", config_file) | ||||
| conf = {} | conf = {} | ||||
| with open(config_file) as stream: | with open(config_file) as stream: | ||||
| conf = yaml.load(stream) | conf = yaml.load(stream) | ||||
| site = Site(sitepath, Config(sitepath, conf)) | site = Site(sitepath, Config(sitepath, conf)) | ||||
| # TODO: Find the appropriate template environment | |||||
| from hyde.ext.templates.jinja import Jinja2Template | |||||
| template = Jinja2Template(sitepath) | |||||
| logger.info("Using [%s] as the template", template) | |||||
| # Configure the environment | |||||
| logger.info("Configuring Template environment") | |||||
| template.configure(site.config) | |||||
| # Prepare site info | |||||
| logger.info("Analyzing site contents") | |||||
| site.build() | |||||
| context = dict(site=site) | |||||
| # Generate site one file at a time | |||||
| logger.info("Generating site to [%s]" % site.config.deploy_root_path) | |||||
| for page in site.content.walk_resources(): | |||||
| logger.info("Processing [%s]", page) | |||||
| target = File(page.source_file.get_mirror(site.config.deploy_root_path, site.content.source_folder)) | |||||
| target.parent.make() | |||||
| if page.source_file.is_text: | |||||
| logger.info("Rendering [%s]", page) | |||||
| context.update(page=page) | |||||
| text = template.render(page.source_file.read_all(), context) | |||||
| target.write(text) | |||||
| else: | |||||
| logger.info("Copying binary file [%s]", page) | |||||
| page.source_file.copy_to(target) | |||||
| from hyde.generator import Generator | |||||
| gen = Generator(site) | |||||
| gen.generate_all() | |||||
| @@ -1,6 +1,18 @@ | |||||
| """ | """ | ||||
| The generator class and related utility functions. | The generator class and related utility functions. | ||||
| """ | """ | ||||
| from hyde.exceptions import HydeException | |||||
| from hyde.fs import File | |||||
| from hyde.template import Template | |||||
| from contextlib import contextmanager | |||||
| import logging | |||||
| from logging import NullHandler | |||||
| logger = logging.getLogger('hyde.engine') | |||||
| logger.addHandler(NullHandler()) | |||||
| class Generator(object): | class Generator(object): | ||||
| """ | """ | ||||
| @@ -10,25 +22,126 @@ class Generator(object): | |||||
| def __init__(self, site): | def __init__(self, site): | ||||
| super(Generator, self).__init__() | super(Generator, self).__init__() | ||||
| self.site = site | self.site = site | ||||
| self.__context__ = dict(site=site) | |||||
| self.template = None | |||||
| @contextmanager | |||||
| def context_for_resource(self, resource): | |||||
| """ | |||||
| Context manager that intializes the context for a given | |||||
| resource and rolls it back after the resource is processed. | |||||
| """ | |||||
| # TODO: update metadata and other resource | |||||
| # specific properties here. | |||||
| self.__context__.update(resource=resource) | |||||
| yield self.__context__ | |||||
| self.__context__.update(resource=None) | |||||
| def initialize_template_if_needed(self): | |||||
| """ | |||||
| Loads and configures the template environement from the site | |||||
| configuration if its not done already. | |||||
| """ | |||||
| if not self.template: | |||||
| logger.info("Generating site at [%s]" % self.site.sitepath) | |||||
| self.template = Template.find_template(self.site) | |||||
| logger.info("Using [%s] as the template", self.template) | |||||
| logger.info("Configuring the template environment") | |||||
| self.template.configure(self.site.config) | |||||
| def rebuild_if_needed(self): | |||||
| """ | |||||
| Checks if the site requries a rebuild and builds if | |||||
| necessary. | |||||
| """ | |||||
| #TODO: Perhaps this is better suited in Site | |||||
| if not len(self.site.content.child_nodes): | |||||
| logger.info("Reading site contents") | |||||
| self.site.build() | |||||
| def generate_all(self): | def generate_all(self): | ||||
| """ | """ | ||||
| Generates the entire website | Generates the entire website | ||||
| """ | """ | ||||
| pass | |||||
| logger.info("Reading site contents") | |||||
| self.initialize_template_if_needed() | |||||
| self.rebuild_if_needed() | |||||
| def generate_node(self, node=None): | |||||
| logger.info("Generating site to [%s]" % | |||||
| self.site.config.deploy_root_path) | |||||
| self.__generate_node__(self.site.content) | |||||
| def generate_node_at_path(self, node_path=None): | |||||
| """ | """ | ||||
| Generates a single node. If node is non-existent or empty | |||||
| Generates a single node. If node_path is non-existent or empty, | |||||
| generates the entire site. | generates the entire site. | ||||
| """ | """ | ||||
| pass | |||||
| self.initialize_template_if_needed() | |||||
| self.rebuild_if_needed() | |||||
| node = None | |||||
| if node_path: | |||||
| node = self.site.content.node_from_path(node_path) | |||||
| self.generate_node(node) | |||||
| def generate_resource(self, resource=None): | |||||
| def generate_node(self, node=None): | |||||
| """ | |||||
| Generates the given node. If node is invalid, empty or | |||||
| non-existent, generates the entire website. | |||||
| """ | """ | ||||
| Generates a single resource. If resource is non-existent or empty | |||||
| self.initialize_template_if_needed() | |||||
| self.rebuild_if_needed() | |||||
| if not node: | |||||
| return self.generate_all() | |||||
| try: | |||||
| self.__generate_node__(node) | |||||
| except HydeException: | |||||
| self.generate_all() | |||||
| def generate_resource_at_path(self, resource_path=None): | |||||
| """ | |||||
| Generates a single resource. If resource_path is non-existent or empty, | |||||
| generats the entire website. | generats the entire website. | ||||
| """ | """ | ||||
| pass | |||||
| self.initialize_template_if_needed() | |||||
| self.rebuild_if_needed() | |||||
| resource = None | |||||
| if resource_path: | |||||
| resource = self.site.content.resource_from_path(resource_path) | |||||
| return self.generate_resource(resource) | |||||
| def generate_resource(self, resource=None): | |||||
| """ | |||||
| Generates the given resource. If resource is invalid, empty or | |||||
| non-existent, generates the entire website. | |||||
| """ | |||||
| self.initialize_template_if_needed() | |||||
| self.rebuild_if_needed() | |||||
| if not resource: | |||||
| return self.generate_all() | |||||
| try: | |||||
| self.__generate_resource__(resource) | |||||
| except HydeException: | |||||
| self.generate_all() | |||||
| def __generate_node__(self, node): | |||||
| logger.info("Generating [%s]", node) | |||||
| for resource in node.walk_resources(): | |||||
| self.__generate_resource__(resource) | |||||
| def __generate_resource__(self, resource): | |||||
| 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: | |||||
| logger.info("Rendering [%s]", resource) | |||||
| text = self.template.render(resource.source_file.read_all(), | |||||
| context) | |||||
| target.write(text) | |||||
| else: | |||||
| logger.info("Copying binary file [%s]", resource) | |||||
| resource.source_file.copy_to(target) | |||||
| @@ -9,7 +9,7 @@ | |||||
| <!--[if (gte IE 9)|!(IE)]><!--> <html lang="en" class="no-js"> <!--<![endif]--> | <!--[if (gte IE 9)|!(IE)]><!--> <html lang="en" class="no-js"> <!--<![endif]--> | ||||
| <head> | <head> | ||||
| {% block starthead %}{% endblock starthead %} | {% block starthead %}{% endblock starthead %} | ||||
| <meta charset="{{page.meta.charset|default('utf-8')}}"> | |||||
| <meta charset="{{resource.meta.charset|default('utf-8')}}"> | |||||
| <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame | <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame | ||||
| Remove this if you use the .htaccess --> | Remove this if you use the .htaccess --> | ||||
| @@ -20,9 +20,9 @@ | |||||
| <!-- meta element for compatibility mode needs to be before all elements except title & meta msdn.microsoft.com/en-us/library/cc288325(VS.85).aspx --> | <!-- meta element for compatibility mode needs to be before all elements except title & meta msdn.microsoft.com/en-us/library/cc288325(VS.85).aspx --> | ||||
| <!-- Chrome Frame is only invoked if meta element for compatibility mode is within the first 1K bytes code.google.com/p/chromium/issues/detail?id=23003 --> | <!-- Chrome Frame is only invoked if meta element for compatibility mode is within the first 1K bytes code.google.com/p/chromium/issues/detail?id=23003 --> | ||||
| <title>{% block title %}{{page.meta.title}}{% endblock %}</title> | |||||
| <meta name="description" content="{{page.meta.description}}"> | |||||
| <meta name="author" content="{{page.meta.author}}"> | |||||
| <title>{% block title %}{{resource.meta.title}}{% endblock %}</title> | |||||
| <meta name="description" content="{{resource.meta.description}}"> | |||||
| <meta name="author" content="{{resource.meta.author}}"> | |||||
| <!-- Mobile viewport optimized: j.mp/bplateviewport --> | <!-- Mobile viewport optimized: j.mp/bplateviewport --> | ||||
| <meta name="viewport" content="{{page.meta.viewport|default('width=device-width, initial-scale=1.0')"> | <meta name="viewport" content="{{page.meta.viewport|default('width=device-width, initial-scale=1.0')"> | ||||
| @@ -46,7 +46,7 @@ | |||||
| {% endblock headjs %} | {% endblock headjs %} | ||||
| {% block endhead %}{% endblock endhead %} | {% block endhead %}{% endblock endhead %} | ||||
| </head> | </head> | ||||
| <body id="{{page.id if page.id else page.name_without_extension}}"> | |||||
| <body id="{{resource.id if resource.id else resource.name_without_extension}}"> | |||||
| {% block content %} | {% block content %} | ||||
| <div id="container"> | <div id="container"> | ||||
| {% block container %} | {% block container %} | ||||
| @@ -4,15 +4,15 @@ | |||||
| <html lang="en"> | <html lang="en"> | ||||
| <head> | <head> | ||||
| {% block starthead %}{% endblock starthead %} | {% block starthead %}{% endblock starthead %} | ||||
| <meta charset="{{page.meta.charset|default('utf-8')}}"> | |||||
| <meta http-equiv="X-UA-Compatible" content="{{page.meta.compatibility|default('IE=edge,chrome=1')}}"> | |||||
| <meta charset="{{resource.meta.charset|default('utf-8')}}"> | |||||
| <meta http-equiv="X-UA-Compatible" content="{{resource.meta.compatibility|default('IE=edge,chrome=1')}}"> | |||||
| <title>{% block title %}{{page.meta.title}}{% endblock %}</title> | |||||
| <meta name="description" content="{{page.meta.description}}"> | |||||
| <meta name="author" content="{{page.meta.author}}"> | |||||
| <title>{% block title %}{{resource.meta.title}}{% endblock %}</title> | |||||
| <meta name="description" content="{{resource.meta.description}}"> | |||||
| <meta name="author" content="{{resource.meta.author}}"> | |||||
| <!-- Mobile viewport optimized: j.mp/bplateviewport --> | <!-- Mobile viewport optimized: j.mp/bplateviewport --> | ||||
| <meta name="viewport" content="{{page.meta.viewport|default('width=device-width, initial-scale=1.0')}}"> | |||||
| <meta name="viewport" content="{{resource.meta.viewport|default('width=device-width, initial-scale=1.0')}}"> | |||||
| {% block favicons %} | {% block favicons %} | ||||
| <!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | <!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | ||||
| @@ -25,7 +25,7 @@ | |||||
| {% endblock css %} | {% endblock css %} | ||||
| {% block endhead %}{% endblock endhead %} | {% block endhead %}{% endblock endhead %} | ||||
| </head> | </head> | ||||
| <body id="{{page.id if page.id else page.name_without_extension}}"> | |||||
| <body id="{{resource.id if resource.id else resource.name_without_extension}}"> | |||||
| {% block content %} | {% block content %} | ||||
| <div id="container"> | <div id="container"> | ||||
| {% block container %} | {% block container %} | ||||
| @@ -32,7 +32,7 @@ class Config(Expando): | |||||
| Represents the hyde configuration file | Represents the hyde configuration file | ||||
| """ | """ | ||||
| def __init__(self, site_path, config_dict=None): | |||||
| def __init__(self, sitepath, config_dict=None): | |||||
| default_config = dict( | default_config = dict( | ||||
| content_root = 'content', | content_root = 'content', | ||||
| deploy_root = 'deploy', | deploy_root = 'deploy', | ||||
| @@ -45,7 +45,7 @@ class Config(Expando): | |||||
| if config_dict: | if config_dict: | ||||
| conf.update(config_dict) | conf.update(config_dict) | ||||
| super(Config, self).__init__(conf) | super(Config, self).__init__(conf) | ||||
| self.site_path = Folder(site_path) | |||||
| self.sitepath = Folder(sitepath) | |||||
| @property | @property | ||||
| @@ -53,25 +53,25 @@ class Config(Expando): | |||||
| """ | """ | ||||
| Derives the deploy root path from the site path | Derives the deploy root path from the site path | ||||
| """ | """ | ||||
| return self.site_path.child_folder(self.deploy_root) | |||||
| return self.sitepath.child_folder(self.deploy_root) | |||||
| @property | @property | ||||
| def content_root_path(self): | def content_root_path(self): | ||||
| """ | """ | ||||
| Derives the content root path from the site path | Derives the content root path from the site path | ||||
| """ | """ | ||||
| return self.site_path.child_folder(self.content_root) | |||||
| return self.sitepath.child_folder(self.content_root) | |||||
| @property | @property | ||||
| def media_root_path(self): | def media_root_path(self): | ||||
| """ | """ | ||||
| Derives the media root path from the site path | Derives the media root path from the site path | ||||
| """ | """ | ||||
| return self.site_path.child_folder(self.media_root) | |||||
| return self.sitepath.child_folder(self.media_root) | |||||
| @property | @property | ||||
| def layout_root_path(self): | def layout_root_path(self): | ||||
| """ | """ | ||||
| Derives the layout root path from the site path | Derives the layout root path from the site path | ||||
| """ | """ | ||||
| return self.site_path.child_folder(self.layout_root) | |||||
| return self.sitepath.child_folder(self.layout_root) | |||||
| @@ -264,10 +264,10 @@ class Site(object): | |||||
| Represents the site to be generated. | Represents the site to be generated. | ||||
| """ | """ | ||||
| def __init__(self, site_path=None, config=None): | |||||
| def __init__(self, sitepath=None, config=None): | |||||
| super(Site, self).__init__() | super(Site, self).__init__() | ||||
| self.site_path = Folder(str(site_path)) | |||||
| self.config = config if config else Config(self.site_path) | |||||
| self.sitepath = Folder(str(sitepath)) | |||||
| self.config = config if config else Config(self.sitepath) | |||||
| self.content = RootNode(self.config.content_root_path, self) | self.content = RootNode(self.config.content_root_path, self) | ||||
| def build(self): | def build(self): | ||||
| @@ -17,6 +17,7 @@ class Template(object): | |||||
| implementations are responsible for transforming this object to match the `settings` | implementations are responsible for transforming this object to match the `settings` | ||||
| required for the template engines. | required for the template engines. | ||||
| """ | """ | ||||
| abstract | abstract | ||||
| def render(self, text, context): | def render(self, text, context): | ||||
| @@ -24,4 +25,15 @@ class Template(object): | |||||
| Given the text, and the context, this function | Given the text, and the context, this function | ||||
| must return the rendered string. | must return the rendered string. | ||||
| """ | """ | ||||
| abstract | |||||
| abstract | |||||
| @staticmethod | |||||
| def find_template(site): | |||||
| """ | |||||
| Reads the configuration to find the appropriate template. | |||||
| """ | |||||
| # TODO: Find the appropriate template environment | |||||
| from hyde.ext.templates.jinja import Jinja2Template | |||||
| template = Jinja2Template(site.sitepath) | |||||
| return template | |||||
| @@ -2,6 +2,7 @@ | |||||
| {% block main %} | {% block main %} | ||||
| Hi! | Hi! | ||||
| I am a test template to make sure jinja2 generation works well with hyde. | I am a test template to make sure jinja2 generation works well with hyde. | ||||
| {{resource.name}} | |||||
| {% endblock %} | {% endblock %} | ||||
| @@ -4,15 +4,15 @@ | |||||
| <html lang="en"> | <html lang="en"> | ||||
| <head> | <head> | ||||
| {% block starthead %}{% endblock starthead %} | {% block starthead %}{% endblock starthead %} | ||||
| <meta charset="{{page.meta.charset|default('utf-8')}}"> | |||||
| <meta http-equiv="X-UA-Compatible" content="{{page.meta.compatibility|default('IE=edge,chrome=1')}}"> | |||||
| <meta charset="{{resource.meta.charset|default('utf-8')}}"> | |||||
| <meta http-equiv="X-UA-Compatible" content="{{resource.meta.compatibility|default('IE=edge,chrome=1')}}"> | |||||
| <title>{% block title %}{{page.meta.title}}{% endblock %}</title> | |||||
| <meta name="description" content="{{page.meta.description}}"> | |||||
| <meta name="author" content="{{page.meta.author}}"> | |||||
| <title>{% block title %}{{resource.meta.title}}{% endblock %}</title> | |||||
| <meta name="description" content="{{resource.meta.description}}"> | |||||
| <meta name="author" content="{{resource.meta.author}}"> | |||||
| <!-- Mobile viewport optimized: j.mp/bplateviewport --> | <!-- Mobile viewport optimized: j.mp/bplateviewport --> | ||||
| <meta name="viewport" content="{{page.meta.viewport|default('width=device-width, initial-scale=1.0')}}"> | |||||
| <meta name="viewport" content="{{resource.meta.viewport|default('width=device-width, initial-scale=1.0')}}"> | |||||
| {% block favicons %} | {% block favicons %} | ||||
| <!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | <!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | ||||
| @@ -25,7 +25,7 @@ | |||||
| {% endblock css %} | {% endblock css %} | ||||
| {% block endhead %}{% endblock endhead %} | {% block endhead %}{% endblock endhead %} | ||||
| </head> | </head> | ||||
| <body id="{{page.id if page.id else page.name_without_extension}}"> | |||||
| <body id="{{resource.id if resource.id else resource.name_without_extension}}"> | |||||
| {% block content %} | {% block content %} | ||||
| <div id="container"> | <div id="container"> | ||||
| {% block container %} | {% block container %} | ||||
| @@ -0,0 +1,37 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| """ | |||||
| Use nose | |||||
| `$ pip install nose` | |||||
| `$ nosetests` | |||||
| """ | |||||
| from hyde.generator import Generator | |||||
| from hyde.fs import FS, File, Folder | |||||
| from hyde.site import Site | |||||
| from nose.tools import raises, with_setup, nottest | |||||
| from pyquery import PyQuery | |||||
| TEST_SITE = File(__file__).parent.child_folder('_test') | |||||
| @nottest | |||||
| def create_test_site(): | |||||
| TEST_SITE.make() | |||||
| TEST_SITE.parent.child_folder('sites/test_jinja').copy_contents_to(TEST_SITE) | |||||
| @nottest | |||||
| def delete_test_site(): | |||||
| TEST_SITE.delete() | |||||
| @with_setup(create_test_site, delete_test_site) | |||||
| def test_generate_resource_from_path(): | |||||
| site = Site(TEST_SITE) | |||||
| site.build() | |||||
| 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 about.exists | |||||
| text = about.read_all() | |||||
| q = PyQuery(text) | |||||
| assert about.name in q("div#main").text() | |||||
| @@ -1,20 +0,0 @@ | |||||
| #!/usr/bin/env python | |||||
| # encoding: utf-8 | |||||
| """ | |||||
| test_generator.py | |||||
| Created by FlowPlayer - Lakshmi Vyas on 2010-12-29. | |||||
| Copyright (c) 2010 __MyCompanyName__. All rights reserved. | |||||
| """ | |||||
| import sys | |||||
| import os | |||||
| def main(): | |||||
| pass | |||||
| if __name__ == '__main__': | |||||
| main() | |||||
| @@ -54,7 +54,7 @@ class TestConfig(object): | |||||
| """ | """ | ||||
| def test_default_configuration(self): | def test_default_configuration(self): | ||||
| c = Config(site_path=TEST_SITE_ROOT) | |||||
| c = Config(sitepath=TEST_SITE_ROOT) | |||||
| for root in ['content', 'layout', 'media']: | for root in ['content', 'layout', 'media']: | ||||
| name = root + '_root' | name = root + '_root' | ||||
| path = name + '_path' | path = name + '_path' | ||||
| @@ -67,11 +67,11 @@ class TestConfig(object): | |||||
| def test_conf1(self): | def test_conf1(self): | ||||
| c = Config(site_path=TEST_SITE_ROOT, config_dict=yaml.load(self.conf1)) | |||||
| c = Config(sitepath=TEST_SITE_ROOT, config_dict=yaml.load(self.conf1)) | |||||
| assert c.content_root_path == TEST_SITE_ROOT.child_folder('stuff') | assert c.content_root_path == TEST_SITE_ROOT.child_folder('stuff') | ||||
| def test_conf2(self): | def test_conf2(self): | ||||
| c = Config(site_path=TEST_SITE_ROOT, config_dict=yaml.load(self.conf2)) | |||||
| c = Config(sitepath=TEST_SITE_ROOT, config_dict=yaml.load(self.conf2)) | |||||
| 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') | ||||
| @@ -91,7 +91,7 @@ class TestSiteWithConfig(object): | |||||
| TEST_SITE_ROOT.copy_contents_to(cls.SITE_PATH) | TEST_SITE_ROOT.copy_contents_to(cls.SITE_PATH) | ||||
| cls.config_file = File(cls.SITE_PATH.child('alternate.yaml')) | cls.config_file = File(cls.SITE_PATH.child('alternate.yaml')) | ||||
| with open(cls.config_file.path) as config: | with open(cls.config_file.path) as config: | ||||
| cls.config = Config(site_path=cls.SITE_PATH, config_dict=yaml.load(config)) | |||||
| cls.config = Config(sitepath=cls.SITE_PATH, config_dict=yaml.load(config)) | |||||
| cls.SITE_PATH.child_folder('content').rename_to(cls.config.content_root) | cls.SITE_PATH.child_folder('content').rename_to(cls.config.content_root) | ||||
| @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: | ||||