Mercurial > piecrust2
diff piecrust/templating/jinja/extensions.py @ 851:2c7e57d80bba
optimize: Don't load Jinja unless we need to.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sat, 29 Apr 2017 21:42:22 -0700 |
parents | |
children | 448710d84121 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/templating/jinja/extensions.py Sat Apr 29 21:42:22 2017 -0700 @@ -0,0 +1,192 @@ +from jinja2.ext import Extension, Markup +from jinja2.lexer import Token, describe_token +from jinja2.nodes import CallBlock, Const +from compressinja.html import HtmlCompressor, StreamProcessContext +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 + + +class PieCrustFormatExtension(Extension): + tags = set(['pcformat']) + + def __init__(self, environment): + super(PieCrustFormatExtension, self).__init__(environment) + + def parse(self, parser): + lineno = next(parser.stream).lineno + args = [parser.parse_expression()] + body = parser.parse_statements(['name:endpcformat'], drop_needle=True) + return CallBlock(self.call_method('_format', args), + [], [], body).set_lineno(lineno) + + def _format(self, format_name, caller=None): + body = caller() + text = format_text(self.environment.app, + format_name, + Markup(body.rstrip()).unescape(), + exact_format=True) + return text + + +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 + + +def get_highlight_css(style_name='default', class_name='.highlight'): + return HtmlFormatter(style=style_name).get_style_defs(class_name) + + +class PieCrustCacheExtension(Extension): + tags = set(['pccache', 'cache']) + + 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 = next(parser.stream).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', 'name:endcache'], + 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 + + exc_stack = self.environment.app.env.exec_info_stack + render_ctx = exc_stack.current_page_info.render_ctx + rdr_pass = render_ctx.current_pass_info + + # try to load the block from the cache + # if there is no fragment in the cache, render it and store + # it in the cache. + pair = self.environment.piecrust_cache.get(key) + if pair is not None: + rdr_pass.used_source_names.update(pair[1]) + return pair[0] + + pair = self.environment.piecrust_cache.get(key) + if pair is not None: + rdr_pass.used_source_names.update(pair[1]) + return pair[0] + + prev_used = rdr_pass.used_source_names.copy() + rv = caller() + after_used = rdr_pass.used_source_names.copy() + used_delta = after_used.difference(prev_used) + self.environment.piecrust_cache[key] = (rv, used_delta) + return rv + + +class PieCrustSpacelessExtension(HtmlCompressor): + """ A re-implementation of `SelectiveHtmlCompressor` so that we can + both use `strip` or `spaceless` in templates. + """ + def filter_stream(self, stream): + ctx = StreamProcessContext(stream) + strip_depth = 0 + while 1: + if stream.current.type == 'block_begin': + for tk in ['strip', 'spaceless']: + change = self._processToken(ctx, stream, tk) + if change != 0: + strip_depth += change + if strip_depth < 0: + ctx.fail('Unexpected tag end%s' % tk) + break + if strip_depth > 0 and stream.current.type == 'data': + ctx.token = stream.current + value = self.normalize(ctx) + yield Token(stream.current.lineno, 'data', value) + else: + yield stream.current + next(stream) + + def _processToken(self, ctx, stream, test_token): + change = 0 + if (stream.look().test('name:%s' % test_token) or + stream.look().test('name:end%s' % test_token)): + stream.skip() + if stream.current.value == test_token: + change = 1 + else: + change = -1 + stream.skip() + if stream.current.type != 'block_end': + ctx.fail('expected end of block, got %s' % + describe_token(stream.current)) + stream.skip() + return change