| @@ -70,36 +70,13 @@ class Engine(Application): | |||
| The generate command. Generates the site at the given deployment directory. | |||
| """ | |||
| sitepath = Folder(args.sitepath) | |||
| logger.info("Generating site at [%s]" % sitepath) | |||
| # Read the configuration | |||
| config_file = sitepath.child(args.config) | |||
| logger.info("Reading site configuration from [%s]", config_file) | |||
| conf = {} | |||
| with open(config_file) as stream: | |||
| conf = yaml.load(stream) | |||
| 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. | |||
| """ | |||
| 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): | |||
| """ | |||
| @@ -10,25 +22,126 @@ class Generator(object): | |||
| def __init__(self, site): | |||
| super(Generator, self).__init__() | |||
| 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): | |||
| """ | |||
| 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. | |||
| """ | |||
| 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. | |||
| """ | |||
| 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]--> | |||
| <head> | |||
| {% 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 | |||
| 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 --> | |||
| <!-- 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 --> | |||
| <meta name="viewport" content="{{page.meta.viewport|default('width=device-width, initial-scale=1.0')"> | |||
| @@ -46,7 +46,7 @@ | |||
| {% endblock headjs %} | |||
| {% block endhead %}{% endblock endhead %} | |||
| </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 %} | |||
| <div id="container"> | |||
| {% block container %} | |||
| @@ -4,15 +4,15 @@ | |||
| <html lang="en"> | |||
| <head> | |||
| {% 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 --> | |||
| <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 %} | |||
| <!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | |||
| @@ -25,7 +25,7 @@ | |||
| {% endblock css %} | |||
| {% block endhead %}{% endblock endhead %} | |||
| </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 %} | |||
| <div id="container"> | |||
| {% block container %} | |||
| @@ -32,7 +32,7 @@ class Config(Expando): | |||
| Represents the hyde configuration file | |||
| """ | |||
| def __init__(self, site_path, config_dict=None): | |||
| def __init__(self, sitepath, config_dict=None): | |||
| default_config = dict( | |||
| content_root = 'content', | |||
| deploy_root = 'deploy', | |||
| @@ -45,7 +45,7 @@ class Config(Expando): | |||
| if config_dict: | |||
| conf.update(config_dict) | |||
| super(Config, self).__init__(conf) | |||
| self.site_path = Folder(site_path) | |||
| self.sitepath = Folder(sitepath) | |||
| @property | |||
| @@ -53,25 +53,25 @@ class Config(Expando): | |||
| """ | |||
| 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 | |||
| def content_root_path(self): | |||
| """ | |||
| 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 | |||
| def media_root_path(self): | |||
| """ | |||
| 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 | |||
| def layout_root_path(self): | |||
| """ | |||
| 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. | |||
| """ | |||
| def __init__(self, site_path=None, config=None): | |||
| def __init__(self, sitepath=None, config=None): | |||
| 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) | |||
| def build(self): | |||
| @@ -17,6 +17,7 @@ class Template(object): | |||
| implementations are responsible for transforming this object to match the `settings` | |||
| required for the template engines. | |||
| """ | |||
| abstract | |||
| def render(self, text, context): | |||
| @@ -24,4 +25,15 @@ class Template(object): | |||
| Given the text, and the context, this function | |||
| 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 %} | |||
| Hi! | |||
| I am a test template to make sure jinja2 generation works well with hyde. | |||
| {{resource.name}} | |||
| {% endblock %} | |||
| @@ -4,15 +4,15 @@ | |||
| <html lang="en"> | |||
| <head> | |||
| {% 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 --> | |||
| <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 %} | |||
| <!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | |||
| @@ -25,7 +25,7 @@ | |||
| {% endblock css %} | |||
| {% block endhead %}{% endblock endhead %} | |||
| </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 %} | |||
| <div id="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): | |||
| c = Config(site_path=TEST_SITE_ROOT) | |||
| c = Config(sitepath=TEST_SITE_ROOT) | |||
| for root in ['content', 'layout', 'media']: | |||
| name = root + '_root' | |||
| path = name + '_path' | |||
| @@ -67,11 +67,11 @@ class TestConfig(object): | |||
| 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') | |||
| 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.media_root_path == TEST_SITE_ROOT.child_folder('mmm') | |||
| 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) | |||
| cls.config_file = File(cls.SITE_PATH.child('alternate.yaml')) | |||
| 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) | |||
| @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: | |||