changeset 1012:576f7ebcd9c0

templating: Add Inukshuk template engine.
author Ludovic Chabant <ludovic@chabant.com>
date Mon, 27 Nov 2017 23:11:15 -0800
parents c4cf3cfe2726
children 6370ab74b2d5
files piecrust/plugins/builtin.py piecrust/templating/_inukshukext.py piecrust/templating/inukshukengine.py requirements.txt
diffstat 4 files changed, 493 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/plugins/builtin.py	Sun Nov 26 22:23:03 2017 -0800
+++ b/piecrust/plugins/builtin.py	Mon Nov 27 23:11:15 2017 -0800
@@ -97,10 +97,12 @@
             BlogDataProvider]
 
     def getTemplateEngines(self):
+        from piecrust.templating.inukshukengine import InukshukTemplateEngine
         from piecrust.templating.jinjaengine import JinjaTemplateEngine
         from piecrust.templating.pystacheengine import PystacheTemplateEngine
 
         return [
+            InukshukTemplateEngine(),
             JinjaTemplateEngine(),
             PystacheTemplateEngine()]
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/templating/_inukshukext.py	Mon Nov 27 23:11:15 2017 -0800
@@ -0,0 +1,297 @@
+import io
+import re
+import time
+from inukshuk.ext import Extension, StatementNode
+from inukshuk.ext.core import filter_make_xml_date, filter_safe
+from inukshuk.lexer import (
+    TOKEN_ID_STRING_SINGLE_QUOTES, TOKEN_ID_STRING_DOUBLE_QUOTES)
+from pygments import highlight
+from pygments.formatters import HtmlFormatter
+from pygments.lexers import get_lexer_by_name, guess_lexer
+from piecrust.data.paginator import Paginator
+from piecrust.rendering import format_text
+
+
+class PieCrustExtension(Extension):
+    def __init__(self, app):
+        self.app = app
+
+    def setupEngine(self, engine):
+        engine.piecrust_app = self.app
+        engine.piecrust_cache = {}
+
+    def getGlobals(self):
+        return {
+            'highlight_css': get_highlight_css}
+
+    def getFilters(self):
+        return {
+            'paginate': self._paginate,
+            'formatwith': self._formatWith,
+            'markdown': lambda v: self._formatWith(v, 'markdown'),
+            'textile': lambda v: self._formatWith(v, 'textile'),
+            'nocache': add_no_cache_parameter,
+            'stripoutertag': strip_outer_tag,
+            'stripslash': strip_slash,
+            'atomdate': filter_make_xml_date,
+            'raw': filter_safe
+        }
+
+    def getTests(self):
+        return {}
+
+    def getStatementNodes(self):
+        return [
+            PieCrustHighlightStatementNode,
+            PieCrustGeshiStatementNode,
+            PieCrustCacheStatementNode,
+            PieCrustFormatStatementNode]
+
+    def _paginate(self, value, items_per_page=5):
+        ctx = self.app.env.render_ctx_stack.current_ctx
+        if ctx is None or ctx.page is None:
+            raise Exception("Can't paginate when no page has been pushed "
+                            "on the execution stack.")
+        return Paginator(ctx.page, value,
+                         sub_num=ctx.sub_num,
+                         items_per_page=items_per_page)
+
+    def _formatWith(self, value, format_name):
+        return format_text(self.app, format_name, value)
+
+
+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 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('/')
+
+
+class PieCrustFormatStatementNode(StatementNode):
+    name = 'pcformat'
+    compiler_imports = ['import io',
+                        'from piecrust.rendering import format_text']
+
+    def __init__(self):
+        super().__init__()
+        self.format = None
+
+    def parse(self, parser):
+        self.format = parser.expectIdentifier()
+        parser.skipWhitespace()
+        parser.expectStatementEnd()
+        parser.parseUntilStatement(self, ['endpcformat'])
+        parser.expectIdentifier('endpcformat')
+
+    def render(self, ctx, data, out):
+        with io.StringIO() as tmp:
+            inner_out = tmp.write
+            for c in self.children:
+                c.render(ctx, data, inner_out)
+
+            text = format_text(ctx.engine.piecrust_app, self.format,
+                               tmp.getvalue(), exact_format=True)
+            out(text)
+
+    def compile(self, ctx, out):
+        out.indent().write('with io.StringIO() as tmp:\n')
+        out.push(False)
+        out.indent().write('prev_out_write = out_write\n')
+        out.indent().write('out_write = tmp.write\n')
+        for c in self.children:
+            c.compile(ctx, out)
+        out.indent().write('out_write = prev_out_write\n')
+        out.indent().write(
+            'text = format_text(ctx_engine.piecrust_app, %s, tmp.getvalue(), '
+            'exact_format=True)\n' % repr(self.format))
+        out.indent().write('out_write(text)\n')
+        out.pull()
+
+
+class PieCrustHighlightStatementNode(StatementNode):
+    name = 'highlight'
+    endname = 'endhighlight'
+    compiler_imports = [
+        'from pygments import highlight',
+        'from pygments.formatters import HtmlFormatter',
+        'from pygments.lexers import get_lexer_by_name, guess_lexer']
+
+    def __init__(self):
+        super().__init__()
+        self.lang = None
+
+    def parse(self, parser):
+        self.lang = parser.expectAny([TOKEN_ID_STRING_SINGLE_QUOTES,
+                                      TOKEN_ID_STRING_DOUBLE_QUOTES])
+        parser.skipWhitespace()
+        parser.expectStatementEnd()
+
+        parser.parseUntilStatement(self, self.endname)
+        parser.expectIdentifier(self.endname)
+
+    def render(self, ctx, data, out):
+        with io.StringIO() as tmp:
+            inner_out = tmp.write
+            for c in self.children:
+                c.render(ctx, data, inner_out)
+
+            raw_text = tmp.getvalue()
+
+        if self.lang is None:
+            lexer = guess_lexer(raw_text)
+        else:
+            lexer = get_lexer_by_name(self.lang, stripall=False)
+
+        formatter = HtmlFormatter()
+        code = highlight(raw_text, lexer, formatter)
+        out(code)
+
+    def compile(self, ctx, out):
+        out.indent().write('with io.StringIO() as tmp:\n')
+        out.push(False)
+        out.indent().write('prev_out_write = out_write\n')
+        out.indent().write('out_write = tmp.write\n')
+        for c in self.children:
+            c.compile(ctx, out)
+        out.indent().write('out_write = prev_out_write\n')
+        out.indent().write('raw_text = tmp.getvalue()\n')
+        out.pull()
+        if self.lang is None:
+            out.indent().write('lexer = guess_lexer(raw_text)\n')
+        else:
+            out.indent().write(
+                'lexer = get_lexer_by_name(%s, stripall=False)\n' %
+                repr(self.lang))
+        out.indent().write('formatter = HtmlFormatter()\n')
+        out.indent().write('code = highlight(raw_text, lexer, formatter)\n')
+        out.indent().write('out_write(code)\n')
+
+
+class PieCrustGeshiStatementNode(PieCrustHighlightStatementNode):
+    name = 'geshi'
+    endname = 'endgeshi'
+
+
+def get_highlight_css(style_name='default', class_name='.highlight'):
+    return HtmlFormatter(style=style_name).get_style_defs(class_name)
+
+
+class PieCrustCacheStatementNode(StatementNode):
+    name = 'pccache'
+    compiler_imports = ['import io']
+
+    def __init__(self):
+        super().__init__()
+        self.cache_key = None
+
+    def parse(self, parser):
+        self.cache_key = parser.expectString()
+        parser.skipWhitespace()
+        parser.expectStatementEnd()
+
+        parser.parseUntilStatement(self, 'endpccache')
+        parser.expectIdentifier('endpccache')
+
+    def render(self, ctx, data, out):
+        raise Exception("No implemented")
+
+        # exc_stack = ctx.engine.piecrust_app.env.exec_info_stack
+        # render_ctx = exc_stack.current_page_info.render_ctx
+        # rdr_pass = render_ctx.current_pass_info
+
+        # pair = ctx.engine.piecrust_cache.get(self.cache_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()
+
+        # with io.StringIO() as tmp:
+        #     inner_out = tmp.write
+        #     for c in self.children:
+        #         c.render(ctx, data, inner_out)
+
+        #     raw_text = tmp.getvalue()
+
+        # after_used = rdr_pass.used_source_names.copy()
+        # used_delta = after_used.difference(prev_used)
+        # ctx.engine.piecrust_cache[self.cache_key] = (raw_text, used_delta)
+
+        # return raw_text
+
+    def compile(self, ctx, out):
+        out.indent().write(
+            'ctx_stack = ctx.engine.piecrust_app.env.render_ctx_stack\n')
+        out.indent().write(
+            'render_ctx = ctx_stack.current_ctx\n')
+        out.indent().write(
+            'rdr_pass = render_ctx.current_pass_info\n')
+
+        pair_var = ctx.varname('pair')
+        out.indent().write(
+            '%s = ctx.engine.piecrust_cache.get(%s)\n' %
+            (pair_var, repr(self.cache_key)))
+        out.indent().write(
+            'if %s is not None:\n' % pair_var)
+        out.push().write(
+            'rdr_pass.used_source_names.update(%s[1])\n' % pair_var)
+        out.indent().write('out_write(%s[0])\n' % pair_var)
+        out.pull()
+        out.indent().write('else:\n')
+
+        tmp_var = ctx.varname('tmp')
+        prev_used_var = ctx.varname('prev_used')
+        prev_out_write_var = ctx.varname('prev_out_write')
+        prev_out_write_escaped_var = ctx.varname('prev_out_write_escaped')
+
+        out.push().write(
+            '%s = rdr_pass.used_source_names.copy()\n' % prev_used_var)
+        out.indent().write(
+            'with io.StringIO() as %s:\n' % tmp_var)
+        out.push().write(
+            '%s = out_write\n' % prev_out_write_var)
+        out.indent().write(
+            '%s = out_write_escaped\n' % prev_out_write_escaped_var)
+        out.indent().write(
+            'out_write = %s.write\n' % tmp_var)
+        out.indent().write(
+            'out_write_escaped = ctx.engine._getWriteEscapeFunc(out_write)\n')
+        for c in self.children:
+            c.compile(ctx, out)
+
+        out.indent().write(
+            'out_write_escaped = %s\n' % prev_out_write_escaped_var)
+        out.indent().write(
+            'out_write = %s\n' % prev_out_write_var)
+        out.indent().write(
+            'raw_text = %s.getvalue()\n' % tmp_var)
+        out.pull()
+
+        out.indent().write(
+            'after_used = rdr_pass.used_source_names.copy()\n')
+        out.indent().write(
+            'used_delta = after_used.difference(%s)\n' % prev_used_var)
+        out.indent().write(
+            'ctx.engine.piecrust_cache[%s] = (raw_text, used_delta)\n' %
+            repr(self.cache_key))
+        out.indent().write('out_write(raw_text)\n')
+        out.pull()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/templating/inukshukengine.py	Mon Nov 27 23:11:15 2017 -0800
@@ -0,0 +1,193 @@
+import io
+import os.path
+import time
+import logging
+from inukshuk.parser import ParserError
+from piecrust.templating.base import (
+    TemplateEngine, TemplatingError, TemplateNotFoundError)
+
+
+logger = logging.getLogger(__name__)
+
+_profile = True
+
+
+class InukshukTemplateEngine(TemplateEngine):
+    ENGINE_NAMES = ['inukshuk', 'inuk']
+    EXTENSIONS = ['html', 'inuk']
+
+    def __init__(self):
+        self.engine = None
+        self.pc_cache = {}
+        self._seg_loader = None
+        self._buf = io.StringIO()
+        self._buf.truncate(2048)
+        if _profile:
+            self._renderTemplate = self._renderTemplateProf
+        else:
+            self._renderTemplate = self._renderTemplateNoProf
+
+    def populateCache(self):
+        self._ensureLoaded()
+
+        used_names = set()
+
+        def _filter_names(name):
+            if name in used_names:
+                return False
+            used_names.add(name)
+            return True
+
+        self.engine.cacheAllTemplates(cache_condition=_filter_names)
+
+    def renderSegment(self, path, segment, data):
+        if not _string_needs_render(segment.content):
+            return segment.content, False
+
+        self._ensureLoaded()
+
+        tpl_name = os.path.relpath(path, self.app.root_dir)
+        try:
+            self._seg_loader.templates[tpl_name] = segment.content
+            tpl = self.engine.getTemplate(tpl_name, memmodule=True)
+            return self._renderTemplate(tpl, data), True
+        except ParserError as pe:
+            raise TemplatingError(pe.message, path, pe.line_num)
+
+    def renderFile(self, paths, data):
+        self._ensureLoaded()
+
+        tpl = None
+        rendered_path = None
+        for p in paths:
+            try:
+                tpl = self.engine.getTemplate(p)
+                rendered_path = p
+                break
+            except Exception:
+                pass
+
+        if tpl is None:
+            raise TemplateNotFoundError()
+
+        try:
+            return self._renderTemplate(tpl, data)
+        except ParserError as pe:
+            raise TemplatingError(pe.message, rendered_path, pe.line_num)
+
+    def _renderTemplateNoProf(self, tpl, data):
+        return tpl.render(data)
+
+    def _renderTemplateIntoNoProf(self, tpl, data):
+        buf = self._buf
+        buf.seek(0)
+        tpl.renderInto(data, buf)
+        buf.flush()
+        size = buf.tell()
+        buf.seek(0)
+        return buf.read(size)
+
+    def _renderTemplateProf(self, tpl, data):
+        stats = self.app.env.stats
+
+        # Code copied from Inukshuk, but with an `out_write` method that
+        # wraps a timer scope.
+        out = []
+
+        def out_write(s):
+            start = time.perf_counter()
+            out.append(s)
+            stats.stepTimerSince('Inukshuk_outWrite', start)
+
+        tpl._renderWithContext(None, data, out_write)
+        return ''.join(out)
+
+    def _ensureLoaded(self):
+        if self.engine is not None:
+            return
+
+        from inukshuk.engine import Engine
+        from inukshuk.loader import (
+            StringsLoader, FileSystemLoader, CompositeLoader)
+        from ._inukshukext import PieCrustExtension
+
+        self._seg_loader = StringsLoader()
+        loader = CompositeLoader([
+            self._seg_loader,
+            FileSystemLoader(self.app.templates_dirs)])
+        self.engine = Engine(loader)
+        self.engine.autoescape = True
+        self.engine.extensions.append(PieCrustExtension(self.app))
+        self.engine.compile_templates = True
+        self.engine.compile_cache_dir = os.path.join(
+            self.app.cache_dir, 'inuk')
+
+        if _profile:
+            # If we're profiling, monkeypatch all the appropriate methods
+            # from the Inukshuk API.
+            stats = self.app.env.stats
+
+            import inukshuk.rendering
+
+            afe = inukshuk.rendering._attr_first_access
+
+            def wafe(ctx, data, prop_name):
+                with stats.timerScope('Inukshuk_query'):
+                    return afe(ctx, data, prop_name)
+
+            inukshuk.rendering._attr_first_access = wafe
+
+            afer = inukshuk.rendering._attr_first_access_root
+
+            def wafer(ctx, ctx_locals, data, ctx_globals, prop_name):
+                with stats.timerScope('Inukshuk_query'):
+                    return afer(ctx, ctx_locals, data, ctx_globals, prop_name)
+
+            inukshuk.rendering._attr_first_access_root = wafer
+
+            i = inukshuk.rendering.RenderContext.invoke
+
+            def wi(ctx, data, out, data_func, *args, **kwargs):
+                with stats.timerScope('Inukshuk_invoke'):
+                    return i(ctx, data, out, data_func, *args, **kwargs)
+
+            inukshuk.rendering.RenderContext.invoke = wi
+
+            import inukshuk.template
+
+            cc = inukshuk.template.Template._compileContent
+
+            def wcc(tpl, force_compiled=False):
+                with stats.timerScope('Inukshuk_templateCompileContent'):
+                    return cc(tpl, force_compiled)
+
+            inukshuk.template.Template._compileContent = wcc
+
+            dr = inukshuk.template.Template._doRender
+
+            def wdr(tpl, ctx, data, out):
+                with stats.timerScope('Inukshuk_templateDoRender'):
+                    return dr(tpl, ctx, data, out)
+
+            inukshuk.template.Template._doRender = wdr
+
+            stats.registerTimer('Inukshuk_query')
+            stats.registerTimer('Inukshuk_invoke')
+            stats.registerTimer('Inukshuk_templateDoRender')
+            stats.registerTimer('Inukshuk_templateCompileContent')
+            stats.registerTimer('Inukshuk_outWrite')
+
+        try:
+            os.makedirs(self.engine.compile_cache_dir)
+        except OSError:
+            pass
+
+
+def _string_needs_render(txt):
+    index = txt.find('{')
+    while index >= 0:
+        ch = txt[index + 1]
+        if ch == '{' or ch == '%':
+            return True
+        index = txt.find('{', index + 1)
+    return False
--- a/requirements.txt	Sun Nov 26 22:23:03 2017 -0800
+++ b/requirements.txt	Mon Nov 27 23:11:15 2017 -0800
@@ -8,6 +8,7 @@
 Flask-IndieAuth==0.0.3.2
 Flask-Login==0.3.2
 idna==2.5
+Inukshuk==0.1.0
 itsdangerous==0.24
 Jinja2==2.9.6
 Markdown==2.6.2