@@ -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 | commando==0.1.1a | ||||
PyYAML==3.09 | PyYAML==3.09 | ||||
Markdown==2.0.3 | Markdown==2.0.3 | ||||
@@ -63,6 +63,33 @@ class Markdown(Extension): | |||||
output = caller().strip() | output = caller().strip() | ||||
return markdown(self.environment, output) | 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 | # pylint: disable-msg=W0104,E0602,W0613,R0201 | ||||
class Jinja2Template(Template): | class Jinja2Template(Template): | ||||
""" | """ | ||||
@@ -72,18 +99,13 @@ class Jinja2Template(Template): | |||||
def __init__(self, sitepath): | def __init__(self, sitepath): | ||||
super(Jinja2Template, self).__init__(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, | undefined=SilentUndefined, | ||||
trim_blocks=True, | trim_blocks=True, | ||||
extensions=[Markdown, | extensions=[Markdown, | ||||
@@ -92,9 +114,14 @@ class Jinja2Template(Template): | |||||
'jinja2.ext.with_']) | 'jinja2.ext.with_']) | ||||
self.env.globals['media_url'] = media_url | self.env.globals['media_url'] = media_url | ||||
self.env.globals['content_url'] = content_url | self.env.globals['content_url'] = content_url | ||||
self.env.extend(config=config) | |||||
self.env.filters['markdown'] = markdown | self.env.filters['markdown'] = markdown | ||||
config = {} | |||||
if hasattr(site, 'config'): | |||||
config = site.config | |||||
self.env.extend(config=config) | |||||
try: | try: | ||||
from typogrify.templatetags import jinja2_filters | from typogrify.templatetags import jinja2_filters | ||||
except ImportError: | except ImportError: | ||||
@@ -236,7 +236,7 @@ class File(FS): | |||||
determine age. | determine age. | ||||
""" | """ | ||||
return File(str(another_file)).last_modified > self.last_modified | |||||
return self.last_modified < File(str(another_file)).last_modified | |||||
@staticmethod | @staticmethod | ||||
def make_temp(text): | def make_temp(text): | ||||
@@ -82,8 +82,9 @@ class Generator(object): | |||||
self.template.__class__.__name__) | self.template.__class__.__name__) | ||||
logger.info("Configuring the template environment") | 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) | self.events.template_loaded(self.template) | ||||
def initialize(self): | def initialize(self): | ||||
@@ -125,7 +126,7 @@ class Generator(object): | |||||
return False | return False | ||||
deps = self.template.get_dependencies(resource.source_file.read_all()) | deps = self.template.get_dependencies(resource.source_file.read_all()) | ||||
if not deps or None in deps: | if not deps or None in deps: | ||||
return True | |||||
return False | |||||
content = self.site.content.source_folder | content = self.site.content.source_folder | ||||
layout = Folder(self.site.sitepath).child_folder('layout') | layout = Folder(self.site.sitepath).child_folder('layout') | ||||
for dep in deps: | for dep in deps: | ||||
@@ -222,8 +223,8 @@ class Generator(object): | |||||
def __generate_node__(self, node): | def __generate_node__(self, node): | ||||
logger.info("Generating [%s]", node) | |||||
for node in node.walk(): | for node in node.walk(): | ||||
logger.info("Generating Node [%s]", node) | |||||
self.events.begin_node(node) | self.events.begin_node(node) | ||||
for resource in node.resources: | for resource in node.resources: | ||||
self.__generate_resource__(resource) | self.__generate_resource__(resource) | ||||
@@ -10,6 +10,7 @@ from BaseHTTPServer import HTTPServer | |||||
from hyde.fs import File, Folder | from hyde.fs import File, Folder | ||||
from hyde.site import Site | from hyde.site import Site | ||||
from hyde.generator import Generator | from hyde.generator import Generator | ||||
from hyde.exceptions import HydeException | |||||
from hyde.util import getLoggerWithNullHandler | from hyde.util import getLoggerWithNullHandler | ||||
logger = getLoggerWithNullHandler('hyde.server') | logger = getLoggerWithNullHandler('hyde.server') | ||||
@@ -39,21 +40,25 @@ class HydeRequestHandler(SimpleHTTPRequestHandler): | |||||
logger.info('Redirecting...[%s]' % new_url) | logger.info('Redirecting...[%s]' % new_url) | ||||
self.redirect(new_url) | self.redirect(new_url) | ||||
else: | else: | ||||
f = File(self.translate_path(self.path)) | |||||
if not f.exists: | |||||
self.do_404() | |||||
else: | |||||
try: | |||||
SimpleHTTPRequestHandler.do_GET(self) | SimpleHTTPRequestHandler.do_GET(self) | ||||
except HydeException: | |||||
self.do_404() | |||||
def translate_path(self, path): | def translate_path(self, path): | ||||
""" | """ | ||||
Finds the absolute path of the requested file by | Finds the absolute path of the requested file by | ||||
referring to the `site` variable in the server. | referring to the `site` variable in the server. | ||||
""" | """ | ||||
path = SimpleHTTPRequestHandler.translate_path(self, path) | |||||
site = self.server.site | site = self.server.site | ||||
result = urlparse.urlparse(self.path) | result = urlparse.urlparse(self.path) | ||||
logger.info("Trying to load file based on request:[%s]" % result.path) | logger.info("Trying to load file based on request:[%s]" % result.path) | ||||
path = result.path.lstrip('/') | 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) | res = site.content.resource_from_relative_deploy_path(path) | ||||
if not res: | if not res: | ||||
@@ -69,6 +74,7 @@ class HydeRequestHandler(SimpleHTTPRequestHandler): | |||||
if not res: | if not res: | ||||
# Nothing much we can do. | # Nothing much we can do. | ||||
logger.error("Cannot load file:[%s]" % path) | logger.error("Cannot load file:[%s]" % path) | ||||
raise HydeException("Cannot load file: [%s]" % path) | |||||
return site.config.deploy_root_path.child(path) | return site.config.deploy_root_path.child(path) | ||||
else: | else: | ||||
@@ -117,31 +123,22 @@ class HydeWebServer(HTTPServer): | |||||
def __init__(self, site, address, port): | def __init__(self, site, address, port): | ||||
self.site = site | self.site = site | ||||
self.site.load() | self.site.load() | ||||
self.exception_count = 0 | |||||
self.generator = Generator(self.site) | self.generator = Generator(self.site) | ||||
HTTPServer.__init__(self, (address, port), | HTTPServer.__init__(self, (address, port), | ||||
HydeRequestHandler) | HydeRequestHandler) | ||||
def __reinit__(self): | |||||
self.site.load() | |||||
self.generator = Generator(self.site) | |||||
self.regenerate() | |||||
def regenerate(self): | def regenerate(self): | ||||
""" | """ | ||||
Regenerates the entire site. | Regenerates the entire site. | ||||
""" | """ | ||||
try: | try: | ||||
logger.info('Regenerating the entire site') | logger.info('Regenerating the entire site') | ||||
self.site.load() | |||||
self.generator.generate_all() | self.generator.generate_all() | ||||
self.exception_count = 0 | |||||
except Exception, exception: | except Exception, exception: | ||||
self.exception_count += 1 | |||||
logger.error('Error occured when regenerating the site [%s]' | logger.error('Error occured when regenerating the site [%s]' | ||||
% exception.message) | % exception.message) | ||||
if self.exception_count <= 1: | |||||
self.__reinit__() | |||||
def generate_resource(self, resource): | def generate_resource(self, resource): | ||||
@@ -154,7 +151,6 @@ class HydeWebServer(HTTPServer): | |||||
try: | try: | ||||
logger.info('Generating resource [%s]' % resource) | logger.info('Generating resource [%s]' % resource) | ||||
self.generator.generate_resource(resource) | self.generator.generate_resource(resource) | ||||
self.exception_count = 0 | |||||
except Exception, exception: | except Exception, exception: | ||||
logger.error( | logger.error( | ||||
'Error [%s] occured when generating the resource [%s]' | 'Error [%s] occured when generating the resource [%s]' | ||||
@@ -291,6 +291,7 @@ class RootNode(Node): | |||||
resource = self.resource_from_path(afile) | resource = self.resource_from_path(afile) | ||||
if resource: | if resource: | ||||
logger.info("Resource exists at [%s]" % resource.relative_path) | logger.info("Resource exists at [%s]" % resource.relative_path) | ||||
return resource | |||||
if not afile.is_descendant_of(self.source_folder): | if not afile.is_descendant_of(self.source_folder): | ||||
raise HydeException("The given file [%s] does not reside" | raise HydeException("The given file [%s] does not reside" | ||||
@@ -4,7 +4,6 @@ | |||||
Abstract classes and utilities for template engines | Abstract classes and utilities for template engines | ||||
""" | """ | ||||
from hyde.exceptions import HydeException | from hyde.exceptions import HydeException | ||||
from hyde.util import getLoggerWithNullHandler | from hyde.util import getLoggerWithNullHandler | ||||
class Template(object): | class Template(object): | ||||
@@ -17,15 +16,22 @@ class Template(object): | |||||
self.sitepath = sitepath | self.sitepath = sitepath | ||||
self.logger = getLoggerWithNullHandler(self.__class__.__name__) | 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 | The config object is a simple YAML object with required settings. The | ||||
template implementations are responsible for transforming this object | template implementations are responsible for transforming this object | ||||
to match the `settings` required for the template engines. | 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 | abstract | ||||
def get_dependencies(self, text): | def get_dependencies(self, text): | ||||
""" | """ | ||||
Finds the dependencies based on the included | 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 datetime import datetime | ||||
from hyde.ext.templates.jinja import Jinja2Template | from hyde.ext.templates.jinja import Jinja2Template | ||||
from hyde.fs import File, Folder | from hyde.fs import File, Folder | ||||
from hyde.site import Site | |||||
from hyde.generator import Generator | |||||
from hyde.model import Config | from hyde.model import Config | ||||
import jinja2 | import jinja2 | ||||
@@ -17,6 +19,9 @@ from random import choice, randrange | |||||
from util import assert_html_equals | from util import assert_html_equals | ||||
import yaml | import yaml | ||||
from pyquery import PyQuery | |||||
from nose.tools import raises, nottest, with_setup | |||||
ROOT = File(__file__).parent | ROOT = File(__file__).parent | ||||
JINJA2 = ROOT.child_folder('templates/jinja2') | JINJA2 = ROOT.child_folder('templates/jinja2') | ||||
@@ -64,7 +69,6 @@ def test_render(): | |||||
source = File(JINJA2.child('index.html')).read_all() | source = File(JINJA2.child('index.html')).read_all() | ||||
html = t.render(source, context) | html = t.render(source, context) | ||||
from pyquery import PyQuery | |||||
actual = PyQuery(html) | actual = PyQuery(html) | ||||
assert actual(".navigation li").length == 30 | assert actual(".navigation li").length == 30 | ||||
assert actual("div.article").length == 20 | assert actual("div.article").length == 20 | ||||
@@ -128,7 +132,92 @@ def test_markdown_with_extensions(): | |||||
{%endmarkdown%} | {%endmarkdown%} | ||||
""" | """ | ||||
t = Jinja2Template(JINJA2.path) | t = Jinja2Template(JINJA2.path) | ||||
s = Site(JINJA2.path) | |||||
c = Config(JINJA2.path, dict(markdown=dict(extensions=['headerid']))) | c = Config(JINJA2.path, dict(markdown=dict(extensions=['headerid']))) | ||||
t.configure(c) | |||||
s.config = c | |||||
t.configure(s) | |||||
html = t.render(source, {}).strip() | html = t.render(source, {}).strip() | ||||
assert html == u'<h3 id="heading_3">Heading 3</h3>' | 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 |