Mercurial > piecrust2
diff piecrust/templating/jinjaengine.py @ 3:f485ba500df3
Gigantic change to basically make PieCrust 2 vaguely functional.
- Serving works, with debug window.
- Baking works, multi-threading, with dependency handling.
- Various things not implemented yet.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sun, 10 Aug 2014 23:43:16 -0700 |
parents | |
children | 474c9882decf |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/templating/jinjaengine.py Sun Aug 10 23:43:16 2014 -0700 @@ -0,0 +1,260 @@ +import re +import time +import logging +import strict_rfc3339 +from jinja2 import Environment, FileSystemLoader, TemplateNotFound +from jinja2.exceptions import TemplateSyntaxError +from jinja2.ext import Extension, Markup +from jinja2.nodes import CallBlock, Const +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import get_lexer_by_name, guess_lexer +from piecrust.rendering import format_text +from piecrust.routing import CompositeRouteFunction +from piecrust.templating.base import TemplateEngine, TemplateNotFoundError + + +logger = logging.getLogger(__name__) + + +class JinjaTemplateEngine(TemplateEngine): + # Name `twig` is for backwards compatibility with PieCrust 1.x. + ENGINE_NAMES = ['jinja', 'jinja2', 'twig'] + EXTENSIONS = ['jinja', 'jinja2', 'twig'] + + def __init__(self): + self.env = None + + def renderString(self, txt, data, filename=None, line_offset=0): + self._ensureLoaded() + tpl = self.env.from_string(txt) + try: + return tpl.render(data) + except TemplateSyntaxError as tse: + tse.lineno += line_offset + if filename: + tse.filename = filename + import sys + _, __, traceback = sys.exc_info() + raise tse, None, traceback + + def renderFile(self, paths, data): + self._ensureLoaded() + tpl = None + logger.debug("Looking for template: %s" % paths) + for p in paths: + try: + tpl = self.env.get_template(p) + break + except TemplateNotFound: + pass + if tpl is None: + raise TemplateNotFoundError() + return tpl.render(data) + + + def _ensureLoaded(self): + if self.env: + return + loader = FileSystemLoader(self.app.templates_dirs) + self.env = PieCrustEnvironment( + self.app, + loader=loader, + extensions=['jinja2.ext.autoescape', + PieCrustHighlightExtension, + PieCrustCacheExtension]) + + +class PieCrustEnvironment(Environment): + def __init__(self, app, *args, **kwargs): + super(PieCrustEnvironment, self).__init__(*args, **kwargs) + self.app = app + self.globals.update({ + 'fail': raise_exception}) + self.filters.update({ + 'formatwith': self._formatWith, + 'markdown': lambda v: self._formatWith(v, 'markdown'), + 'textile': lambda v: self._formatWith(v, 'textile'), + 'nocache': add_no_cache_parameter, + 'wordcount': get_word_count, + 'stripoutertag': strip_outer_tag, + 'stripslash': strip_slash, + 'titlecase': title_case, + 'atomdate': get_atom_date, + 'date': get_date}) + # Backwards compatibility with PieCrust 1.x. + self.globals.update({ + 'pcfail': raise_exception}) + + # Backwards compatibility with Twig. + twig_compatibility_mode = app.config.get('jinja/twig_compatibility') + if twig_compatibility_mode is None or twig_compatibility_mode is True: + self.trim_blocks = True + self.filters['raw'] = self.filters['safe'] + + # Add route functions. + for route in app.routes: + name = route.template_func_name + func = self.globals.get(name) + if func is None: + func = CompositeRouteFunction() + func.addFunc(route) + self.globals[name] = func + elif isinstance(func, CompositeRouteFunction): + self.globals[name].addFunc(route) + else: + raise Exception("Route function '%s' collides with an " + "existing function or template data." % + name) + + def _formatWith(self, value, format_name): + return format_text(self.app, format_name, value) + + +def raise_exception(msg): + raise Exception(msg) + + +def add_no_cache_parameter(value, param_name='t', param_value=None): + if not param_value: + param_value = time.time() + if '?' in value: + value += '&' + else: + value += '?' + value += '%s=%s' % (param_name, param_value) + return value + + +def get_word_count(value): + return len(value.split()) + + +def strip_outer_tag(value, tag=None): + tag_pattern = '[a-z]+[a-z0-9]*' + if tag is not None: + tag_pattern = re.escape(tag) + pat = r'^\<' + tag_pattern + r'\>(.*)\</' + tag_pattern + '>$' + m = re.match(pat, value) + if m: + return m.group(1) + return value + + +def strip_slash(value): + return value.rstrip('/') + + +def title_case(value): + return value.title() + + +def get_atom_date(value): + return strict_rfc3339.timestamp_to_rfc3339_localoffset(int(value)) + + +def get_date(value, fmt): + return time.strftime(fmt, time.localtime(value)) + + +class PieCrustHighlightExtension(Extension): + tags = set(['highlight', 'geshi']) + + def __init__(self, environment): + super(PieCrustHighlightExtension, self).__init__(environment) + + def parse(self, parser): + lineno = next(parser.stream).lineno + + # Extract the language name. + args = [parser.parse_expression()] + + # Extract optional arguments. + kwarg_names = {'line_numbers': 0, 'use_classes': 0, 'class': 1, 'id': 1} + kwargs = {} + while not parser.stream.current.test('block_end'): + name = parser.stream.expect('name') + if name.value not in kwarg_names: + raise Exception("'%s' is not a valid argument for the code " + "highlighting tag." % name.value) + if kwarg_names[name.value] == 0: + kwargs[name.value] = Const(True) + elif parser.stream.skip_if('assign'): + kwargs[name.value] = parser.parse_expression() + + # body of the block + body = parser.parse_statements(['name:endhighlight', 'name:endgeshi'], + drop_needle=True) + + return CallBlock(self.call_method('_highlight', args, kwargs), + [], [], body).set_lineno(lineno) + + def _highlight(self, lang, line_numbers=False, use_classes=False, + css_class=None, css_id=None, caller=None): + # Try to be mostly compatible with Jinja2-highlight's settings. + body = caller() + + if lang is None: + lexer = guess_lexer(body) + else: + lexer = get_lexer_by_name(lang, stripall=False) + + if css_class is None: + try: + css_class = self.environment.jinja2_highlight_cssclass + except AttributeError: + pass + + if css_class is not None: + formatter = HtmlFormatter(cssclass=css_class, + linenos=line_numbers) + else: + formatter = HtmlFormatter(linenos=line_numbers) + + code = highlight(Markup(body.rstrip()).unescape(), lexer, formatter) + return code + + +class PieCrustCacheExtension(Extension): + tags = set(['pccache']) + + def __init__(self, environment): + super(PieCrustCacheExtension, self).__init__(environment) + + environment.extend( + piecrust_cache_prefix='', + piecrust_cache={} + ) + + def parse(self, parser): + # the first token is the token that started the tag. In our case + # we only listen to ``'pccache'`` so this will be a name token with + # `pccache` as value. We get the line number so that we can give + # that line number to the nodes we create by hand. + lineno = parser.stream.next().lineno + + # now we parse a single expression that is used as cache key. + args = [parser.parse_expression()] + + # now we parse the body of the cache block up to `endpccache` and + # drop the needle (which would always be `endpccache` in that case) + body = parser.parse_statements(['name:endpccache'], drop_needle=True) + + # now return a `CallBlock` node that calls our _cache_support + # helper method on this extension. + return CallBlock(self.call_method('_cache_support', args), + [], [], body).set_lineno(lineno) + + def _cache_support(self, name, caller): + key = self.environment.piecrust_cache_prefix + name + + # try to load the block from the cache + # if there is no fragment in the cache, render it and store + # it in the cache. + rv = self.environment.piecrust_cache.get(key) + if rv is not None: + return rv + rv = caller() + self.environment.piecrust_cache[key] = rv + return rv +