| @@ -3,7 +3,7 @@ Jinja template utilties | |||
| """ | |||
| from hyde.fs import File, Folder | |||
| from hyde.template import Template | |||
| from hyde.template import HtmlWrap, Template | |||
| from jinja2 import contextfunction, Environment, FileSystemLoader | |||
| from jinja2 import environmentfilter, Markup, Undefined, nodes | |||
| from jinja2.ext import Extension | |||
| @@ -11,6 +11,9 @@ from jinja2.exceptions import TemplateError | |||
| class SilentUndefined(Undefined): | |||
| """ | |||
| A redefinition of undefined that eats errors. | |||
| """ | |||
| def __getattr__(self, name): | |||
| return self | |||
| @@ -19,20 +22,28 @@ class SilentUndefined(Undefined): | |||
| def __call__(self, *args, **kwargs): | |||
| return self | |||
| @contextfunction | |||
| def media_url(context, path): | |||
| """ | |||
| Returns the media url given a partial path. | |||
| """ | |||
| site = context['site'] | |||
| return Folder(site.config.media_url).child(path) | |||
| @contextfunction | |||
| def content_url(context, path): | |||
| """ | |||
| Returns the content url given a partial path. | |||
| """ | |||
| site = context['site'] | |||
| return Folder(site.config.base_url).child(path) | |||
| @environmentfilter | |||
| def markdown(env, value): | |||
| """ | |||
| Markdown filter with support for extensions. | |||
| """ | |||
| try: | |||
| import markdown | |||
| except ImportError: | |||
| @@ -46,35 +57,53 @@ def markdown(env, value): | |||
| return md.convert(output) | |||
| class Markdown(Extension): | |||
| """ | |||
| A wrapper around the markdown filter for syntactic sugar. | |||
| """ | |||
| tags = set(['markdown']) | |||
| def parse(self, parser): | |||
| """ | |||
| Parses the statements and defers to the callback for markdown processing. | |||
| """ | |||
| lineno = parser.stream.next().lineno | |||
| body = parser.parse_statements(['name:endmarkdown'], drop_needle=True) | |||
| return nodes.CallBlock( | |||
| self.call_method('_render_markdown', [], [], None, None), | |||
| [], [], body | |||
| ).set_lineno(lineno) | |||
| self.call_method('_render_markdown'), | |||
| [], [], body).set_lineno(lineno) | |||
| def _render_markdown(self, caller=None): | |||
| """ | |||
| Calls the markdown filter to transform the output. | |||
| """ | |||
| if not caller: | |||
| return '' | |||
| output = caller().strip() | |||
| return markdown(self.environment, output) | |||
| class IncludeText(Extension): | |||
| """ | |||
| Automatically runs `markdown` and `typogrify` on included | |||
| files. | |||
| """ | |||
| tags = set(['includetext']) | |||
| def parse(self, parser): | |||
| """ | |||
| Delegates all the parsing to the native include node. | |||
| """ | |||
| node = parser.parse_include() | |||
| return nodes.CallBlock( | |||
| self.call_method('_render_include_text', [], [], None, None), | |||
| [], [], [node] | |||
| ).set_lineno(node.lineno) | |||
| self.call_method('_render_include_text'), | |||
| [], [], [node]).set_lineno(node.lineno) | |||
| def _render_include_text(self, caller=None): | |||
| """ | |||
| Runs markdown and if available, typogrigy on the | |||
| content returned by the include node. | |||
| """ | |||
| if not caller: | |||
| return '' | |||
| output = caller().strip() | |||
| @@ -84,8 +113,89 @@ class IncludeText(Extension): | |||
| output = typo(output) | |||
| return output | |||
| MARKINGS = '_markings_' | |||
| class Reference(Extension): | |||
| """ | |||
| Marks a block in a template such that its available for use | |||
| when referenced using a `refer` tag. | |||
| """ | |||
| tags = set(['mark', 'reference']) | |||
| def parse(self, parser): | |||
| """ | |||
| Parse the variable name that the content must be assigned to. | |||
| """ | |||
| token = parser.stream.next() | |||
| lineno = token.lineno | |||
| tag = token.value | |||
| name = parser.stream.next().value | |||
| body = parser.parse_statements(['name:end%s' % tag], drop_needle=True) | |||
| return nodes.CallBlock( | |||
| self.call_method('_render_output', | |||
| args=[nodes.Name(MARKINGS, 'load'), nodes.Const(name)]), | |||
| [], [], body).set_lineno(lineno) | |||
| def _render_output(self, markings, name, caller=None): | |||
| if not caller: | |||
| return '' | |||
| out = caller() | |||
| if isinstance(markings, dict): | |||
| markings[name] = out | |||
| return out | |||
| class Refer(Extension): | |||
| """ | |||
| Imports content blocks specified in the referred template as | |||
| variables in a given namespace. | |||
| """ | |||
| tags = set(['refer']) | |||
| def parse(self, parser): | |||
| """ | |||
| Parse the referred template and the namespace. | |||
| """ | |||
| token = parser.stream.next() | |||
| lineno = token.lineno | |||
| tag = token.value | |||
| parser.stream.expect('name:to') | |||
| template = parser.parse_expression() | |||
| parser.stream.expect('name:as') | |||
| namespace = parser.stream.next().value | |||
| includeNode = nodes.Include(lineno=lineno) | |||
| includeNode.with_context = True | |||
| includeNode.ignore_missing = False | |||
| includeNode.template = template | |||
| return [ | |||
| nodes.Assign(nodes.Name(MARKINGS, 'store'), nodes.Const({})), | |||
| nodes.Assign(nodes.Name(namespace, 'store'), nodes.Const({})), | |||
| nodes.CallBlock( | |||
| self.call_method('_assign_reference', | |||
| args=[ | |||
| nodes.Name(MARKINGS, 'load'), | |||
| nodes.Name(namespace, 'load')]), | |||
| [], [], [includeNode]).set_lineno(lineno)] | |||
| def _assign_reference(self, markings, namespace, caller): | |||
| """ | |||
| Assign the processed variables into the | |||
| given namespace. | |||
| """ | |||
| out = caller() | |||
| for key, value in markings.items(): | |||
| namespace[key] = value | |||
| namespace['html'] = HtmlWrap(out) | |||
| return '' | |||
| class HydeLoader(FileSystemLoader): | |||
| """ | |||
| A wrapper around the file system loader that performs | |||
| hyde specific tweaks. | |||
| """ | |||
| def __init__(self, sitepath, site, preprocessor=None): | |||
| config = site.config if hasattr(site, 'config') else None | |||
| @@ -101,6 +211,9 @@ class HydeLoader(FileSystemLoader): | |||
| self.preprocessor = preprocessor | |||
| def get_source(self, environment, template): | |||
| """ | |||
| Calls the plugins to preprocess prior to returning the source. | |||
| """ | |||
| (contents, | |||
| filename, | |||
| date) = super(HydeLoader, self).get_source( | |||
| @@ -121,22 +234,29 @@ class Jinja2Template(Template): | |||
| def __init__(self, sitepath): | |||
| super(Jinja2Template, self).__init__(sitepath) | |||
| def configure(self, site, preprocessor=None, postprocessor=None): | |||
| def configure(self, site, engine=None): | |||
| """ | |||
| Uses the site object to initialize the jinja environment. | |||
| """ | |||
| self.site = site | |||
| self.engine = engine | |||
| preprocessor = (engine.preprocessor | |||
| if hasattr(engine, 'preprocessor') else None) | |||
| self.loader = HydeLoader(self.sitepath, site, preprocessor) | |||
| self.env = Environment(loader=self.loader, | |||
| undefined=SilentUndefined, | |||
| trim_blocks=True, | |||
| extensions=[IncludeText, | |||
| Markdown, | |||
| Reference, | |||
| Refer, | |||
| 'jinja2.ext.do', | |||
| 'jinja2.ext.loopcontrols', | |||
| 'jinja2.ext.with_']) | |||
| self.env.globals['media_url'] = media_url | |||
| self.env.globals['content_url'] = content_url | |||
| self.env.globals['engine'] = engine | |||
| self.env.filters['markdown'] = markdown | |||
| config = {} | |||
| @@ -171,6 +291,9 @@ class Jinja2Template(Template): | |||
| @property | |||
| def exception_class(self): | |||
| """ | |||
| The exception to throw. Used by plugins. | |||
| """ | |||
| return TemplateError | |||
| def render(self, text, context): | |||
| @@ -70,11 +70,31 @@ class Generator(object): | |||
| yield self.__context__ | |||
| self.__context__.update(resource=None) | |||
| def context_for_path(self, path): | |||
| resource = self.site.resource_from_path(path) | |||
| if not resource: | |||
| return {} | |||
| ctx = self.__context__.copy | |||
| ctx.resource = resource | |||
| return ctx | |||
| def load_template_if_needed(self): | |||
| """ | |||
| Loads and configures the template environement from the site | |||
| configuration if its not done already. | |||
| """ | |||
| class GeneratorProxy(object): | |||
| """ | |||
| An interface to templates and plugins for | |||
| providing restricted access to the methods. | |||
| """ | |||
| def __init__(self, preprocessor=None, postprocessor=None, context_for_path=None): | |||
| self.preprocessor = preprocessor | |||
| self.postprocessor = postprocessor | |||
| self.context_for_path = context_for_path | |||
| if not self.template: | |||
| logger.info("Generating site at [%s]" % self.site.sitepath) | |||
| self.template = Template.find_template(self.site) | |||
| @@ -83,8 +103,10 @@ class Generator(object): | |||
| logger.info("Configuring the template environment") | |||
| self.template.configure(self.site, | |||
| preprocessor=self.events.begin_text_resource, | |||
| postprocessor=self.events.text_resource_complete) | |||
| engine=GeneratorProxy( | |||
| context_for_path=self.context_for_path, | |||
| preprocessor=self.events.begin_text_resource, | |||
| postprocessor=self.events.text_resource_complete)) | |||
| self.events.template_loaded(self.template) | |||
| def initialize(self): | |||
| @@ -6,6 +6,32 @@ Abstract classes and utilities for template engines | |||
| from hyde.exceptions import HydeException | |||
| from hyde.util import getLoggerWithNullHandler | |||
| class HtmlWrap(object): | |||
| """ | |||
| A wrapper class for raw html. | |||
| Provides pyquery interface if available. | |||
| Otherwise raw html access. | |||
| """ | |||
| def __init__(self, html): | |||
| super(HtmlWrap, self).__init__() | |||
| self.raw = html | |||
| try: | |||
| from pyquery import PyQuery | |||
| except: | |||
| PyQuery = False | |||
| if PyQuery: | |||
| self.q = PyQuery(html) | |||
| def __unicode__(self): | |||
| return self.raw | |||
| def __call__(self, selector=None): | |||
| if not self.q: | |||
| return self.raw | |||
| return self.q(selector).html() | |||
| class Template(object): | |||
| """ | |||
| Interface for hyde template engines. To use a different template engine, | |||
| @@ -16,19 +42,27 @@ class Template(object): | |||
| self.sitepath = sitepath | |||
| self.logger = getLoggerWithNullHandler(self.__class__.__name__) | |||
| def configure(self, config, preprocessor=None, postprocessor=None): | |||
| def configure(self, site, engine): | |||
| """ | |||
| 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 site object should contain a config attribute. 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. | |||
| The engine is an informal protocol to provide access to some | |||
| hyde internals. | |||
| Note that the processor must only be used when referencing templates, | |||
| The preprocessor and postprocessor attributes must contain the | |||
| functions 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 processors must only be used when referencing templates, | |||
| for example, using the include tag. The regular preprocessing and | |||
| post processing logic is handled by hyde. | |||
| A context_for_path attribute must contain the function that returns the | |||
| context object that is populated with the appropriate variables for the given | |||
| path. | |||
| """ | |||
| abstract | |||
| @@ -142,15 +142,6 @@ def test_markdown_with_extensions(): | |||
| 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() | |||
| @nottest | |||
| def assert_markdown_typogrify_processed_well(include_text, includer_text): | |||
| site = Site(TEST_SITE) | |||
| @@ -168,11 +159,19 @@ def assert_markdown_typogrify_processed_well(include_text, includer_text): | |||
| assert "This is a" in q("h1").text() | |||
| assert "heading" in q("h1").text() | |||
| assert q(".amp").length == 1 | |||
| return html | |||
| class TestJinjaTemplate(object): | |||
| def setUp(self): | |||
| TEST_SITE.make() | |||
| TEST_SITE.parent.child_folder('sites/test_jinja').copy_contents_to(TEST_SITE) | |||
| @with_setup(create_test_site, delete_test_site) | |||
| def test_can_include_templates_with_processing(): | |||
| text = """ | |||
| def tearDown(self): | |||
| TEST_SITE.delete() | |||
| def test_can_include_templates_with_processing(self): | |||
| text = """ | |||
| === | |||
| is_processable: False | |||
| === | |||
| @@ -187,29 +186,96 @@ Hyde & Jinja. | |||
| """ | |||
| text2 = """ | |||
| {% include "inc.md" %} | |||
| text2 = """{% include "inc.md" %}""" | |||
| assert_markdown_typogrify_processed_well(text, text2) | |||
| def test_includetext(self): | |||
| text = """ | |||
| === | |||
| is_processable: False | |||
| === | |||
| This is a heading | |||
| ================= | |||
| Hyde & Jinja. | |||
| """ | |||
| assert_markdown_typogrify_processed_well(text, text2) | |||
| text2 = """{% includetext "inc.md" %}""" | |||
| assert_markdown_typogrify_processed_well(text, text2) | |||
| @with_setup(create_test_site, delete_test_site) | |||
| def test_includetext(): | |||
| text = """ | |||
| def test_reference_is_noop(self): | |||
| text = """ | |||
| === | |||
| is_processable: False | |||
| === | |||
| {% mark heading %} | |||
| This is a heading | |||
| ================= | |||
| {% endmark %} | |||
| {% reference content %} | |||
| Hyde & Jinja. | |||
| {% endreference %} | |||
| """ | |||
| text2 = """{% includetext "inc.md" %}""" | |||
| html = assert_markdown_typogrify_processed_well(text, text2) | |||
| assert "mark" not in html | |||
| assert "reference" not in html | |||
| def test_refer(self): | |||
| text = """ | |||
| === | |||
| is_processable: False | |||
| === | |||
| {% filter markdown|typogrify %} | |||
| {% mark heading %} | |||
| This is a heading | |||
| ================= | |||
| {% endmark %} | |||
| {% reference content %} | |||
| Hyde & Jinja. | |||
| {% endreference %} | |||
| {% endfilter %} | |||
| """ | |||
| text2 = """ | |||
| {% refer to "inc.md" as inc %} | |||
| {% filter markdown|typogrify %} | |||
| {{ inc.heading }} | |||
| {{ inc.content }} | |||
| {% endfilter %} | |||
| """ | |||
| html = assert_markdown_typogrify_processed_well(text, text2) | |||
| assert "mark" not in html | |||
| assert "reference" not in html | |||
| text2 = """ | |||
| {% includetext "inc.md" %} | |||
| def test_refer_with_full_html(self): | |||
| text = """ | |||
| === | |||
| is_processable: False | |||
| === | |||
| <div class="fulltext"> | |||
| {% filter markdown|typogrify %} | |||
| {% mark heading %} | |||
| This is a heading | |||
| ================= | |||
| {% endmark %} | |||
| {% reference content %} | |||
| Hyde & Jinja. | |||
| {% endreference %} | |||
| {% endfilter %} | |||
| </div> | |||
| """ | |||
| text2 = """ | |||
| {% refer to "inc.md" as inc %} | |||
| {{ inc.html('.fulltext') }} | |||
| """ | |||
| assert_markdown_typogrify_processed_well(text, text2) | |||
| html = assert_markdown_typogrify_processed_well(text, text2) | |||
| assert "mark" not in html | |||
| assert "reference" not in html | |||