From 0ae758bcda0bedebdd41f193020bf2c2267ebcbf Mon Sep 17 00:00:00 2001 From: Lakshmi Vyasarajan Date: Fri, 21 Jan 2011 19:39:39 +0530 Subject: [PATCH] Added hyde reference tags --- hyde/ext/templates/jinja.py | 141 ++++++++++++++++++++++++++++-- hyde/generator.py | 26 +++++- hyde/template.py | 50 +++++++++-- hyde/tests/test_jinja2template.py | 108 ++++++++++++++++++----- 4 files changed, 285 insertions(+), 40 deletions(-) diff --git a/hyde/ext/templates/jinja.py b/hyde/ext/templates/jinja.py index f514092..0afd74e 100644 --- a/hyde/ext/templates/jinja.py +++ b/hyde/ext/templates/jinja.py @@ -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): diff --git a/hyde/generator.py b/hyde/generator.py index 99dafa1..9c53465 100644 --- a/hyde/generator.py +++ b/hyde/generator.py @@ -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): diff --git a/hyde/template.py b/hyde/template.py index 38e927c..f384882 100644 --- a/hyde/template.py +++ b/hyde/template.py @@ -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 diff --git a/hyde/tests/test_jinja2template.py b/hyde/tests/test_jinja2template.py index 8985acd..f689a89 100644 --- a/hyde/tests/test_jinja2template.py +++ b/hyde/tests/test_jinja2template.py @@ -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 +=== +
+{% filter markdown|typogrify %} +{% mark heading %} +This is a heading +================= +{% endmark %} +{% reference content %} +Hyde & Jinja. +{% endreference %} +{% endfilter %} +
+""" + 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 \ No newline at end of file