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