@@ -0,0 +1,82 @@ | |||
# A brand new **hyde** | |||
This is the new version of hyde under active development. | |||
I haven't managed to document the features yet. [This][hyde1-0] should | |||
give a good understanding of the motivation behind this version. You can | |||
also take a look at the [cloudpanic source][cp] for a reference implementation. | |||
[hyde1-0]: http://groups.google.com/group/hyde-dev/web/hyde-1-0 | |||
[cp]: github.com/tipiirai/cloudpanic/tree/refactor | |||
[Here](http://groups.google.com/group/hyde-dev/browse_thread/thread/2a143bd2081b3322) is | |||
the initial announcement of the project. | |||
# Installation | |||
Hyde supports both python 2.7 and 2.6. | |||
pip install -r req-2.6.txt | |||
or | |||
pip install -r req-2.7.txt | |||
will install all the dependencies of hyde. | |||
You can choose to install hyde by running | |||
python setup.py install | |||
# Creating a new hyde site | |||
The new version of Hyde uses the `argparse` module and hence support subcommands. | |||
hyde -s ~/test_site create -l test | |||
will create a new hyde site using the test layout. | |||
# Generating the hyde site | |||
cd ~/test_site | |||
hyde gen | |||
# Serving the website | |||
cd ~/test_site | |||
hyde serve | |||
open http://localhost:8080 | |||
The server also regenerates on demand. As long as the server is running, | |||
you can make changes to your source and refresh the browser to view the changes. | |||
# A brief list of features | |||
1. Support for multiple templates (although only `Jinja2` is currently implemented) | |||
2. The different processor modules in the previous version are now | |||
replaced by a plugin object. This allows plugins to listen to events that | |||
occur during different times in the lifecycle and respond accordingly. | |||
3. Metadata: Hyde now supports hierarchical metadata. You can specify and override | |||
variables at the site, node or the page level and access them in the templates. | |||
4. Sorting: The sorter plugin provides rich sorting options that extend the | |||
object model. | |||
5. Syntactic Sugar: Because of the richness of the plugin infrastructure, hyde can | |||
now provide additional syntactic sugar to make the content more readable. See | |||
`blockdown` and `autoextend` plugin for examples. | |||
# Next Steps | |||
1. Documentation | |||
2. Default Layouts | |||
3. Django Support | |||
4. Plugins: | |||
* Tags | |||
* Atom / RSS | |||
* Media Compressor | |||
* Image optimizer |
@@ -1,4 +1,3 @@ | |||
# argparse - needed for 2.6 | |||
commando==0.1.1a | |||
PyYAML==3.09 | |||
Markdown==2.0.3 | |||
@@ -63,6 +63,33 @@ class Markdown(Extension): | |||
output = caller().strip() | |||
return markdown(self.environment, output) | |||
class HydeLoader(FileSystemLoader): | |||
def __init__(self, sitepath, site, preprocessor=None): | |||
config = site.config if hasattr(site, 'config') else None | |||
if config: | |||
super(HydeLoader, self).__init__([ | |||
str(config.content_root_path), | |||
str(config.layout_root_path), | |||
]) | |||
else: | |||
super(HydeLoader, self).__init__(str(sitepath)) | |||
self.site = site | |||
self.preprocessor = preprocessor | |||
def get_source(self, environment, template): | |||
(contents, | |||
filename, | |||
date) = super(HydeLoader, self).get_source( | |||
environment, template) | |||
if self.preprocessor: | |||
resource = self.site.content.resource_from_relative_path(template) | |||
if resource: | |||
contents = self.preprocessor(resource, contents) or contents | |||
return (contents, filename, date) | |||
# pylint: disable-msg=W0104,E0602,W0613,R0201 | |||
class Jinja2Template(Template): | |||
""" | |||
@@ -72,18 +99,13 @@ class Jinja2Template(Template): | |||
def __init__(self, sitepath): | |||
super(Jinja2Template, self).__init__(sitepath) | |||
def configure(self, config): | |||
def configure(self, site, preprocessor=None, postprocessor=None): | |||
""" | |||
Uses the config object to initialize the jinja environment. | |||
Uses the site object to initialize the jinja environment. | |||
""" | |||
if config: | |||
loader = FileSystemLoader([ | |||
str(config.content_root_path), | |||
str(config.layout_root_path), | |||
]) | |||
else: | |||
loader = FileSystemLoader(str(self.sitepath)) | |||
self.env = Environment(loader=loader, | |||
self.site = site | |||
self.loader = HydeLoader(self.sitepath, site, preprocessor) | |||
self.env = Environment(loader=self.loader, | |||
undefined=SilentUndefined, | |||
trim_blocks=True, | |||
extensions=[Markdown, | |||
@@ -92,9 +114,14 @@ class Jinja2Template(Template): | |||
'jinja2.ext.with_']) | |||
self.env.globals['media_url'] = media_url | |||
self.env.globals['content_url'] = content_url | |||
self.env.extend(config=config) | |||
self.env.filters['markdown'] = markdown | |||
config = {} | |||
if hasattr(site, 'config'): | |||
config = site.config | |||
self.env.extend(config=config) | |||
try: | |||
from typogrify.templatetags import jinja2_filters | |||
except ImportError: | |||
@@ -236,7 +236,7 @@ class File(FS): | |||
determine age. | |||
""" | |||
return File(str(another_file)).last_modified > self.last_modified | |||
return self.last_modified < File(str(another_file)).last_modified | |||
@staticmethod | |||
def make_temp(text): | |||
@@ -82,8 +82,9 @@ class Generator(object): | |||
self.template.__class__.__name__) | |||
logger.info("Configuring the template environment") | |||
self.template.configure(self.site.config) | |||
self.template.configure(self.site, | |||
preprocessor=self.events.begin_text_resource, | |||
postprocessor=self.events.text_resource_complete) | |||
self.events.template_loaded(self.template) | |||
def initialize(self): | |||
@@ -125,7 +126,7 @@ class Generator(object): | |||
return False | |||
deps = self.template.get_dependencies(resource.source_file.read_all()) | |||
if not deps or None in deps: | |||
return True | |||
return False | |||
content = self.site.content.source_folder | |||
layout = Folder(self.site.sitepath).child_folder('layout') | |||
for dep in deps: | |||
@@ -222,8 +223,8 @@ class Generator(object): | |||
def __generate_node__(self, node): | |||
logger.info("Generating [%s]", node) | |||
for node in node.walk(): | |||
logger.info("Generating Node [%s]", node) | |||
self.events.begin_node(node) | |||
for resource in node.resources: | |||
self.__generate_resource__(resource) | |||
@@ -10,6 +10,7 @@ from BaseHTTPServer import HTTPServer | |||
from hyde.fs import File, Folder | |||
from hyde.site import Site | |||
from hyde.generator import Generator | |||
from hyde.exceptions import HydeException | |||
from hyde.util import getLoggerWithNullHandler | |||
logger = getLoggerWithNullHandler('hyde.server') | |||
@@ -39,21 +40,25 @@ class HydeRequestHandler(SimpleHTTPRequestHandler): | |||
logger.info('Redirecting...[%s]' % new_url) | |||
self.redirect(new_url) | |||
else: | |||
f = File(self.translate_path(self.path)) | |||
if not f.exists: | |||
self.do_404() | |||
else: | |||
try: | |||
SimpleHTTPRequestHandler.do_GET(self) | |||
except HydeException: | |||
self.do_404() | |||
def translate_path(self, path): | |||
""" | |||
Finds the absolute path of the requested file by | |||
referring to the `site` variable in the server. | |||
""" | |||
path = SimpleHTTPRequestHandler.translate_path(self, path) | |||
site = self.server.site | |||
result = urlparse.urlparse(self.path) | |||
logger.info("Trying to load file based on request:[%s]" % result.path) | |||
path = result.path.lstrip('/') | |||
if path.strip() == "" or File(path).kind.strip() == "": | |||
return site.config.deploy_root_path.child(path) | |||
res = site.content.resource_from_relative_deploy_path(path) | |||
if not res: | |||
@@ -69,6 +74,7 @@ class HydeRequestHandler(SimpleHTTPRequestHandler): | |||
if not res: | |||
# Nothing much we can do. | |||
logger.error("Cannot load file:[%s]" % path) | |||
raise HydeException("Cannot load file: [%s]" % path) | |||
return site.config.deploy_root_path.child(path) | |||
else: | |||
@@ -117,31 +123,22 @@ class HydeWebServer(HTTPServer): | |||
def __init__(self, site, address, port): | |||
self.site = site | |||
self.site.load() | |||
self.exception_count = 0 | |||
self.generator = Generator(self.site) | |||
HTTPServer.__init__(self, (address, port), | |||
HydeRequestHandler) | |||
def __reinit__(self): | |||
self.site.load() | |||
self.generator = Generator(self.site) | |||
self.regenerate() | |||
def regenerate(self): | |||
""" | |||
Regenerates the entire site. | |||
""" | |||
try: | |||
logger.info('Regenerating the entire site') | |||
self.site.load() | |||
self.generator.generate_all() | |||
self.exception_count = 0 | |||
except Exception, exception: | |||
self.exception_count += 1 | |||
logger.error('Error occured when regenerating the site [%s]' | |||
% exception.message) | |||
if self.exception_count <= 1: | |||
self.__reinit__() | |||
def generate_resource(self, resource): | |||
@@ -154,7 +151,6 @@ class HydeWebServer(HTTPServer): | |||
try: | |||
logger.info('Generating resource [%s]' % resource) | |||
self.generator.generate_resource(resource) | |||
self.exception_count = 0 | |||
except Exception, exception: | |||
logger.error( | |||
'Error [%s] occured when generating the resource [%s]' | |||
@@ -291,6 +291,7 @@ class RootNode(Node): | |||
resource = self.resource_from_path(afile) | |||
if resource: | |||
logger.info("Resource exists at [%s]" % resource.relative_path) | |||
return resource | |||
if not afile.is_descendant_of(self.source_folder): | |||
raise HydeException("The given file [%s] does not reside" | |||
@@ -4,7 +4,6 @@ | |||
Abstract classes and utilities for template engines | |||
""" | |||
from hyde.exceptions import HydeException | |||
from hyde.util import getLoggerWithNullHandler | |||
class Template(object): | |||
@@ -17,15 +16,22 @@ class Template(object): | |||
self.sitepath = sitepath | |||
self.logger = getLoggerWithNullHandler(self.__class__.__name__) | |||
def configure(self, config): | |||
def configure(self, config, preprocessor=None, postprocessor=None): | |||
""" | |||
The config object is a simple YAML object with required settings. The | |||
template implementations are responsible for transforming this object | |||
to match the `settings` required for the template engines. | |||
""" | |||
The preprocessor and postprocessor contain the fucntions that | |||
trigger the hyde plugins to preprocess the template after load | |||
and postprocess it after it is processed and code is generated. | |||
Note that the processor must only be used when referencing templates, | |||
for example, using the include tag. The regular preprocessing and | |||
post processing logic is handled by hyde. | |||
""" | |||
abstract | |||
def get_dependencies(self, text): | |||
""" | |||
Finds the dependencies based on the included | |||
@@ -9,6 +9,8 @@ Code borrowed from rwbench.py from the jinja2 examples | |||
from datetime import datetime | |||
from hyde.ext.templates.jinja import Jinja2Template | |||
from hyde.fs import File, Folder | |||
from hyde.site import Site | |||
from hyde.generator import Generator | |||
from hyde.model import Config | |||
import jinja2 | |||
@@ -17,6 +19,9 @@ from random import choice, randrange | |||
from util import assert_html_equals | |||
import yaml | |||
from pyquery import PyQuery | |||
from nose.tools import raises, nottest, with_setup | |||
ROOT = File(__file__).parent | |||
JINJA2 = ROOT.child_folder('templates/jinja2') | |||
@@ -64,7 +69,6 @@ def test_render(): | |||
source = File(JINJA2.child('index.html')).read_all() | |||
html = t.render(source, context) | |||
from pyquery import PyQuery | |||
actual = PyQuery(html) | |||
assert actual(".navigation li").length == 30 | |||
assert actual("div.article").length == 20 | |||
@@ -128,7 +132,92 @@ def test_markdown_with_extensions(): | |||
{%endmarkdown%} | |||
""" | |||
t = Jinja2Template(JINJA2.path) | |||
s = Site(JINJA2.path) | |||
c = Config(JINJA2.path, dict(markdown=dict(extensions=['headerid']))) | |||
t.configure(c) | |||
s.config = c | |||
t.configure(s) | |||
html = t.render(source, {}).strip() | |||
assert html == u'<h3 id="heading_3">Heading 3</h3>' | |||
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_can_include_templates_with_processing(): | |||
text = """ | |||
=== | |||
is_processable: False | |||
=== | |||
{% filter typogrify %}{% markdown %} | |||
This is a heading | |||
================= | |||
Hyde & Jinja. | |||
{% endmarkdown %}{% endfilter %} | |||
""" | |||
text2 = """ | |||
{% include "inc.md" %} | |||
""" | |||
site = Site(TEST_SITE) | |||
site.config.plugins = ['hyde.ext.plugins.meta.MetaPlugin'] | |||
inc = File(TEST_SITE.child('content/inc.md')) | |||
inc.write(text) | |||
site.load() | |||
gen = Generator(site) | |||
gen.load_template_if_needed() | |||
template = gen.template | |||
html = template.render(text2, {}).strip() | |||
assert html | |||
q = PyQuery(html) | |||
assert "is_processable" not in html | |||
assert "This is a" in q("h1").text() | |||
assert "heading" in q("h1").text() | |||
assert q(".amp").length == 1 | |||
#@with_setup(create_test_site, delete_test_site) | |||
@nottest | |||
def test_includetext(): | |||
text = """ | |||
=== | |||
is_processable: False | |||
=== | |||
This is a heading | |||
================= | |||
An "&". | |||
""" | |||
text2 = """ | |||
{% includetext inc.md %} | |||
""" | |||
site = Site(TEST_SITE) | |||
inc = File(TEST_SITE.child('content/inc.md')) | |||
inc.write(text) | |||
site.load() | |||
gen = Generator(site) | |||
gen.load_template_if_needed() | |||
template = gen.template | |||
html = template.render(text2, {}).strip() | |||
assert html | |||
q = PyQuery(html) | |||
assert q("h1").length == 1 | |||
assert q(".amp").length == 1 |
@@ -0,0 +1,2 @@ | |||
argparse | |||
-r req-2.7.txt |
@@ -0,0 +1,7 @@ | |||
commando==0.1.1a | |||
PyYAML==3.09 | |||
Markdown==2.0.3 | |||
MarkupSafe==0.11 | |||
smartypants==1.6.0.3 | |||
-e git://github.com/hydepy/typogrify.git#egg=typogrify | |||
Jinja2==2.5.5 |