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