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