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