Browse Source

Added hyde reference tags

main
Lakshmi Vyasarajan 14 years ago
parent
commit
0ae758bcda
4 changed files with 285 additions and 40 deletions
  1. +132
    -9
      hyde/ext/templates/jinja.py
  2. +24
    -2
      hyde/generator.py
  3. +42
    -8
      hyde/template.py
  4. +87
    -21
      hyde/tests/test_jinja2template.py

+ 132
- 9
hyde/ext/templates/jinja.py View File

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


+ 24
- 2
hyde/generator.py View File

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


+ 42
- 8
hyde/template.py View File

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



+ 87
- 21
hyde/tests/test_jinja2template.py View File

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

Loading…
Cancel
Save