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