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