@@ -3,7 +3,7 @@ Jinja template utilties | |||||
""" | """ | ||||
from hyde.fs import File, Folder | 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 contextfunction, Environment, FileSystemLoader | ||||
from jinja2 import environmentfilter, Markup, Undefined, nodes | from jinja2 import environmentfilter, Markup, Undefined, nodes | ||||
from jinja2.ext import Extension | from jinja2.ext import Extension | ||||
@@ -11,6 +11,9 @@ from jinja2.exceptions import TemplateError | |||||
class SilentUndefined(Undefined): | class SilentUndefined(Undefined): | ||||
""" | |||||
A redefinition of undefined that eats errors. | |||||
""" | |||||
def __getattr__(self, name): | def __getattr__(self, name): | ||||
return self | return self | ||||
@@ -19,20 +22,28 @@ class SilentUndefined(Undefined): | |||||
def __call__(self, *args, **kwargs): | def __call__(self, *args, **kwargs): | ||||
return self | return self | ||||
@contextfunction | @contextfunction | ||||
def media_url(context, path): | def media_url(context, path): | ||||
""" | |||||
Returns the media url given a partial path. | |||||
""" | |||||
site = context['site'] | site = context['site'] | ||||
return Folder(site.config.media_url).child(path) | return Folder(site.config.media_url).child(path) | ||||
@contextfunction | @contextfunction | ||||
def content_url(context, path): | def content_url(context, path): | ||||
""" | |||||
Returns the content url given a partial path. | |||||
""" | |||||
site = context['site'] | site = context['site'] | ||||
return Folder(site.config.base_url).child(path) | return Folder(site.config.base_url).child(path) | ||||
@environmentfilter | @environmentfilter | ||||
def markdown(env, value): | def markdown(env, value): | ||||
""" | |||||
Markdown filter with support for extensions. | |||||
""" | |||||
try: | try: | ||||
import markdown | import markdown | ||||
except ImportError: | except ImportError: | ||||
@@ -46,35 +57,53 @@ def markdown(env, value): | |||||
return md.convert(output) | return md.convert(output) | ||||
class Markdown(Extension): | class Markdown(Extension): | ||||
""" | |||||
A wrapper around the markdown filter for syntactic sugar. | |||||
""" | |||||
tags = set(['markdown']) | tags = set(['markdown']) | ||||
def parse(self, parser): | def parse(self, parser): | ||||
""" | |||||
Parses the statements and defers to the callback for markdown processing. | |||||
""" | |||||
lineno = parser.stream.next().lineno | lineno = parser.stream.next().lineno | ||||
body = parser.parse_statements(['name:endmarkdown'], drop_needle=True) | body = parser.parse_statements(['name:endmarkdown'], drop_needle=True) | ||||
return nodes.CallBlock( | 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): | def _render_markdown(self, caller=None): | ||||
""" | |||||
Calls the markdown filter to transform the output. | |||||
""" | |||||
if not caller: | if not caller: | ||||
return '' | return '' | ||||
output = caller().strip() | output = caller().strip() | ||||
return markdown(self.environment, output) | return markdown(self.environment, output) | ||||
class IncludeText(Extension): | class IncludeText(Extension): | ||||
""" | |||||
Automatically runs `markdown` and `typogrify` on included | |||||
files. | |||||
""" | |||||
tags = set(['includetext']) | tags = set(['includetext']) | ||||
def parse(self, parser): | def parse(self, parser): | ||||
""" | |||||
Delegates all the parsing to the native include node. | |||||
""" | |||||
node = parser.parse_include() | node = parser.parse_include() | ||||
return nodes.CallBlock( | 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): | def _render_include_text(self, caller=None): | ||||
""" | |||||
Runs markdown and if available, typogrigy on the | |||||
content returned by the include node. | |||||
""" | |||||
if not caller: | if not caller: | ||||
return '' | return '' | ||||
output = caller().strip() | output = caller().strip() | ||||
@@ -84,8 +113,89 @@ class IncludeText(Extension): | |||||
output = typo(output) | output = typo(output) | ||||
return 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): | class HydeLoader(FileSystemLoader): | ||||
""" | |||||
A wrapper around the file system loader that performs | |||||
hyde specific tweaks. | |||||
""" | |||||
def __init__(self, sitepath, site, preprocessor=None): | def __init__(self, sitepath, site, preprocessor=None): | ||||
config = site.config if hasattr(site, 'config') else None | config = site.config if hasattr(site, 'config') else None | ||||
@@ -101,6 +211,9 @@ class HydeLoader(FileSystemLoader): | |||||
self.preprocessor = preprocessor | self.preprocessor = preprocessor | ||||
def get_source(self, environment, template): | def get_source(self, environment, template): | ||||
""" | |||||
Calls the plugins to preprocess prior to returning the source. | |||||
""" | |||||
(contents, | (contents, | ||||
filename, | filename, | ||||
date) = super(HydeLoader, self).get_source( | date) = super(HydeLoader, self).get_source( | ||||
@@ -121,22 +234,29 @@ class Jinja2Template(Template): | |||||
def __init__(self, sitepath): | def __init__(self, sitepath): | ||||
super(Jinja2Template, self).__init__(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. | Uses the site object to initialize the jinja environment. | ||||
""" | """ | ||||
self.site = site | self.site = site | ||||
self.engine = engine | |||||
preprocessor = (engine.preprocessor | |||||
if hasattr(engine, 'preprocessor') else None) | |||||
self.loader = HydeLoader(self.sitepath, site, preprocessor) | self.loader = HydeLoader(self.sitepath, site, preprocessor) | ||||
self.env = Environment(loader=self.loader, | self.env = Environment(loader=self.loader, | ||||
undefined=SilentUndefined, | undefined=SilentUndefined, | ||||
trim_blocks=True, | trim_blocks=True, | ||||
extensions=[IncludeText, | extensions=[IncludeText, | ||||
Markdown, | Markdown, | ||||
Reference, | |||||
Refer, | |||||
'jinja2.ext.do', | 'jinja2.ext.do', | ||||
'jinja2.ext.loopcontrols', | 'jinja2.ext.loopcontrols', | ||||
'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.globals['engine'] = engine | |||||
self.env.filters['markdown'] = markdown | self.env.filters['markdown'] = markdown | ||||
config = {} | config = {} | ||||
@@ -171,6 +291,9 @@ class Jinja2Template(Template): | |||||
@property | @property | ||||
def exception_class(self): | def exception_class(self): | ||||
""" | |||||
The exception to throw. Used by plugins. | |||||
""" | |||||
return TemplateError | return TemplateError | ||||
def render(self, text, context): | def render(self, text, context): | ||||
@@ -70,11 +70,31 @@ class Generator(object): | |||||
yield self.__context__ | yield self.__context__ | ||||
self.__context__.update(resource=None) | 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): | def load_template_if_needed(self): | ||||
""" | """ | ||||
Loads and configures the template environement from the site | Loads and configures the template environement from the site | ||||
configuration if its not done already. | 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: | if not self.template: | ||||
logger.info("Generating site at [%s]" % self.site.sitepath) | logger.info("Generating site at [%s]" % self.site.sitepath) | ||||
self.template = Template.find_template(self.site) | self.template = Template.find_template(self.site) | ||||
@@ -83,8 +103,10 @@ class Generator(object): | |||||
logger.info("Configuring the template environment") | logger.info("Configuring the template environment") | ||||
self.template.configure(self.site, | 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) | self.events.template_loaded(self.template) | ||||
def initialize(self): | def initialize(self): | ||||
@@ -6,6 +6,32 @@ 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 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): | class Template(object): | ||||
""" | """ | ||||
Interface for hyde template engines. To use a different template engine, | Interface for hyde template engines. To use a different template engine, | ||||
@@ -16,19 +42,27 @@ 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, 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 | for example, using the include tag. The regular preprocessing and | ||||
post processing logic is handled by hyde. | 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 | abstract | ||||
@@ -142,15 +142,6 @@ def test_markdown_with_extensions(): | |||||
TEST_SITE = File(__file__).parent.child_folder('_test') | 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 | @nottest | ||||
def assert_markdown_typogrify_processed_well(include_text, includer_text): | def assert_markdown_typogrify_processed_well(include_text, includer_text): | ||||
site = Site(TEST_SITE) | 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 "This is a" in q("h1").text() | ||||
assert "heading" in q("h1").text() | assert "heading" in q("h1").text() | ||||
assert q(".amp").length == 1 | 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 | 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 | is_processable: False | ||||
=== | === | ||||
{% mark heading %} | |||||
This is a 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. | 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 |