Mercurial > piecrust2
changeset 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 | 370e74941d32 |
children | 4850f8c21b6e cd236a6af9f6 |
files | piecrust/templating/jinja/__init__.py piecrust/templating/jinja/environment.py piecrust/templating/jinja/extensions.py piecrust/templating/jinja/loader.py piecrust/templating/jinjaengine.py |
diffstat | 4 files changed, 447 insertions(+), 430 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/templating/jinja/environment.py Sat Apr 29 21:42:22 2017 -0700 @@ -0,0 +1,204 @@ +import re +import time +import email.utils +import hashlib +import strict_rfc3339 +from jinja2 import Environment +from .extensions import get_highlight_css +from piecrust.data.paginator import Paginator +from piecrust.rendering import format_text +from piecrust.uriutil import multi_replace + + +class PieCrustEnvironment(Environment): + def __init__(self, app, *args, **kwargs): + self.app = app + + # Before we create the base Environement, let's figure out the options + # we want to pass to it. + twig_compatibility_mode = app.config.get('jinja/twig_compatibility') + + # Disable auto-reload when we're baking. + if app.config.get('baker/is_baking'): + kwargs.setdefault('auto_reload', False) + + # Let the user override most Jinja options via the site config. + for name in ['block_start_string', 'block_end_string', + 'variable_start_string', 'variable_end_string', + 'comment_start_string', 'comment_end_string', + 'line_statement_prefix', 'line_comment_prefix', + 'trim_blocks', 'lstrip_blocks', + 'newline_sequence', 'keep_trailing_newline']: + val = app.config.get('jinja/' + name) + if val is not None: + kwargs.setdefault(name, val) + + # Twig trims blocks. + if twig_compatibility_mode is True: + kwargs['trim_blocks'] = True + + # All good! Create the Environment. + super(PieCrustEnvironment, self).__init__(*args, **kwargs) + + # Now add globals and filters. + self.globals.update({ + 'now': get_now_date(), + 'fail': raise_exception, + 'highlight_css': get_highlight_css}) + + self.filters.update({ + 'keys': get_dict_keys, + 'values': get_dict_values, + '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, + 'wordcount': get_word_count, + 'stripoutertag': strip_outer_tag, + 'stripslash': strip_slash, + 'titlecase': title_case, + 'md5': make_md5, + 'atomdate': get_xml_date, + 'xmldate': get_xml_date, + 'emaildate': get_email_date, + 'date': get_date}) + + # Backwards compatibility with Twig. + if twig_compatibility_mode is True: + self.filters['raw'] = self.filters['safe'] + self.globals['pcfail'] = raise_exception + + def _paginate(self, value, items_per_page=5): + cpi = self.app.env.exec_info_stack.current_page_info + if cpi is None or cpi.page is None or cpi.render_ctx is None: + raise Exception("Can't paginate when no page has been pushed " + "on the execution stack.") + return Paginator(cpi.page, value, + page_num=cpi.render_ctx.page_num, + items_per_page=items_per_page) + + def _formatWith(self, value, format_name): + return format_text(self.app, format_name, value) + + +def raise_exception(msg): + raise Exception(msg) + + +def get_dict_keys(value): + if isinstance(value, list): + return [i[0] for i in value] + return value.keys() + + +def get_dict_values(value): + if isinstance(value, list): + return [i[1] for i in value] + return value.values() + + +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 make_md5(value): + return hashlib.md5(value.lower().encode('utf8')).hexdigest() + + +def get_xml_date(value): + """ Formats timestamps like 1985-04-12T23:20:50.52Z + """ + if value == 'now': + value = time.time() + return strict_rfc3339.timestamp_to_rfc3339_localoffset(int(value)) + + +def get_email_date(value, localtime=False): + """ Formats timestamps like Fri, 09 Nov 2001 01:08:47 -0000 + """ + if value == 'now': + value = time.time() + return email.utils.formatdate(value, localtime=localtime) + + +def get_now_date(): + return time.time() + + +def get_date(value, fmt): + if value == 'now': + value = time.time() + if '%' not in fmt: + suggest = php_format_to_strftime_format(fmt) + if suggest != fmt: + suggest_message = ("You probably want a format that looks " + "like: '%s'." % suggest) + else: + suggest_message = ("We can't suggest a proper date format " + "for you right now, though.") + raise Exception("Got incorrect date format: '%s\n" + "PieCrust 1 date formats won't work in PieCrust 2. " + "%s\n" + "Please check the `strftime` formatting page here: " + "https://docs.python.org/3/library/datetime.html" + "#strftime-and-strptime-behavior" % + (fmt, suggest_message)) + return time.strftime(fmt, time.localtime(value)) + + +def php_format_to_strftime_format(fmt): + replacements = { + 'd': '%d', + 'D': '%a', + 'j': '%d', + 'l': '%A', + 'w': '%w', + 'z': '%j', + 'W': '%W', + 'F': '%B', + 'm': '%m', + 'M': '%b', + 'n': '%m', + 'y': '%Y', + 'Y': '%y', + 'g': '%I', + 'G': '%H', + 'h': '%I', + 'H': '%H', + 'i': '%M', + 's': '%S', + 'e': '%Z', + 'O': '%z', + 'c': '%Y-%m-%dT%H:%M:%SZ'} + return multi_replace(fmt, replacements)
--- /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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/piecrust/templating/jinja/loader.py Sat Apr 29 21:42:22 2017 -0700 @@ -0,0 +1,24 @@ +import os.path +from jinja2 import FileSystemLoader + + +class PieCrustLoader(FileSystemLoader): + def __init__(self, searchpath, encoding='utf-8'): + super(PieCrustLoader, self).__init__(searchpath, encoding) + self.segment_parts_cache = {} + + def get_source(self, environment, template): + if template.startswith('$part='): + filename, seg_part = self.segment_parts_cache[template] + + mtime = os.path.getmtime(filename) + + def uptodate(): + try: + return os.path.getmtime(filename) == mtime + except OSError: + return False + + return seg_part, filename, uptodate + + return super(PieCrustLoader, self).get_source(environment, template)
--- a/piecrust/templating/jinjaengine.py Sat Apr 29 21:27:33 2017 -0700 +++ b/piecrust/templating/jinjaengine.py Sat Apr 29 21:42:22 2017 -0700 @@ -1,25 +1,8 @@ -import re -import time import os.path -import hashlib import logging -import email.utils -import strict_rfc3339 -from jinja2 import Environment, FileSystemLoader, TemplateNotFound -from jinja2.exceptions import TemplateSyntaxError -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.data.paginator import Paginator from piecrust.environment import AbortedSourceUseError -from piecrust.rendering import format_text from piecrust.templating.base import (TemplateEngine, TemplateNotFoundError, TemplatingError) -from piecrust.uriutil import multi_replace logger = logging.getLogger(__name__) @@ -32,6 +15,8 @@ def __init__(self): self.env = None + self._jinja_syntax_error = None + self._jinja_not_found = None def renderSegmentPart(self, path, seg_part, data): self._ensureLoaded() @@ -41,17 +26,17 @@ part_path = _make_segment_part_path(path, seg_part.offset) self.env.loader.segment_parts_cache[part_path] = ( - path, seg_part.content) + path, seg_part.content) try: tpl = self.env.get_template(part_path) - except TemplateSyntaxError as tse: + except self._jinja_syntax_error as tse: raise self._getTemplatingError(tse, filename=path) - except TemplateNotFound: + except self._jinja_not_found: raise TemplateNotFoundError() try: return tpl.render(data) - except TemplateSyntaxError as tse: + except self._jinja_syntax_error as tse: raise self._getTemplatingError(tse) except AbortedSourceUseError: raise @@ -72,9 +57,9 @@ tpl = self.env.get_template(p) rendered_path = p break - except TemplateSyntaxError as tse: + except self._jinja_syntax_error as tse: raise self._getTemplatingError(tse) - except TemplateNotFound: + except self._jinja_not_found: pass if tpl is None: @@ -82,7 +67,7 @@ try: return tpl.render(data) - except TemplateSyntaxError as tse: + except self._jinja_syntax_error as tse: raise self._getTemplatingError(tse) except AbortedSourceUseError: raise @@ -118,11 +103,14 @@ ext_names.append('autoescape') # Create the final list of extensions. + from piecrust.templating.jinja.extensions import ( + PieCrustHighlightExtension, PieCrustCacheExtension, + PieCrustSpacelessExtension, PieCrustFormatExtension) extensions = [ - PieCrustHighlightExtension, - PieCrustCacheExtension, - PieCrustSpacelessExtension, - PieCrustFormatExtension] + PieCrustHighlightExtension, + PieCrustCacheExtension, + PieCrustSpacelessExtension, + PieCrustFormatExtension] for n in ext_names: if '.' not in n: n = 'jinja2.ext.' + n @@ -133,11 +121,19 @@ # Create the Jinja environment. logger.debug("Creating Jinja environment with folders: %s" % self.app.templates_dirs) + from piecrust.templating.jinja.loader import PieCrustLoader loader = PieCrustLoader(self.app.templates_dirs) + from piecrust.templating.jinja.environment import PieCrustEnvironment self.env = PieCrustEnvironment( - self.app, - loader=loader, - extensions=extensions) + self.app, + loader=loader, + extensions=extensions) + + # Get types we need later. + from jinja2 import TemplateNotFound + from jinja2.exceptions import TemplateSyntaxError + self._jinja_syntax_error = TemplateSyntaxError + self._jinja_not_found = TemplateNotFound def _string_needs_render(txt): @@ -154,402 +150,3 @@ return '$part=%s:%d' % (path, start) -class PieCrustLoader(FileSystemLoader): - def __init__(self, searchpath, encoding='utf-8'): - super(PieCrustLoader, self).__init__(searchpath, encoding) - self.segment_parts_cache = {} - - def get_source(self, environment, template): - if template.startswith('$part='): - filename, seg_part = self.segment_parts_cache[template] - - mtime = os.path.getmtime(filename) - - def uptodate(): - try: - return os.path.getmtime(filename) == mtime - except OSError: - return False - - return seg_part, filename, uptodate - - return super(PieCrustLoader, self).get_source(environment, template) - - -class PieCrustEnvironment(Environment): - def __init__(self, app, *args, **kwargs): - self.app = app - - # Before we create the base Environement, let's figure out the options - # we want to pass to it. - twig_compatibility_mode = app.config.get('jinja/twig_compatibility') - - # Disable auto-reload when we're baking. - if app.config.get('baker/is_baking'): - kwargs.setdefault('auto_reload', False) - - # Let the user override most Jinja options via the site config. - for name in ['block_start_string', 'block_end_string', - 'variable_start_string', 'variable_end_string', - 'comment_start_string', 'comment_end_string', - 'line_statement_prefix', 'line_comment_prefix', - 'trim_blocks', 'lstrip_blocks', - 'newline_sequence', 'keep_trailing_newline']: - val = app.config.get('jinja/' + name) - if val is not None: - kwargs.setdefault(name, val) - - # Twig trims blocks. - if twig_compatibility_mode is True: - kwargs['trim_blocks'] = True - - # All good! Create the Environment. - super(PieCrustEnvironment, self).__init__(*args, **kwargs) - - # Now add globals and filters. - self.globals.update({ - 'now': get_now_date(), - 'fail': raise_exception, - 'highlight_css': get_highlight_css}) - - self.filters.update({ - 'keys': get_dict_keys, - 'values': get_dict_values, - '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, - 'wordcount': get_word_count, - 'stripoutertag': strip_outer_tag, - 'stripslash': strip_slash, - 'titlecase': title_case, - 'md5': make_md5, - 'atomdate': get_xml_date, - 'xmldate': get_xml_date, - 'emaildate': get_email_date, - 'date': get_date}) - - # Backwards compatibility with Twig. - if twig_compatibility_mode is True: - self.filters['raw'] = self.filters['safe'] - self.globals['pcfail'] = raise_exception - - def _paginate(self, value, items_per_page=5): - cpi = self.app.env.exec_info_stack.current_page_info - if cpi is None or cpi.page is None or cpi.render_ctx is None: - raise Exception("Can't paginate when no page has been pushed " - "on the execution stack.") - return Paginator(cpi.page, value, - page_num=cpi.render_ctx.page_num, - items_per_page=items_per_page) - - def _formatWith(self, value, format_name): - return format_text(self.app, format_name, value) - - -def raise_exception(msg): - raise Exception(msg) - - -def get_dict_keys(value): - if isinstance(value, list): - return [i[0] for i in value] - return value.keys() - - -def get_dict_values(value): - if isinstance(value, list): - return [i[1] for i in value] - return value.values() - - -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 make_md5(value): - return hashlib.md5(value.lower().encode('utf8')).hexdigest() - - -def get_xml_date(value): - """ Formats timestamps like 1985-04-12T23:20:50.52Z - """ - if value == 'now': - value = time.time() - return strict_rfc3339.timestamp_to_rfc3339_localoffset(int(value)) - - -def get_email_date(value, localtime=False): - """ Formats timestamps like Fri, 09 Nov 2001 01:08:47 -0000 - """ - if value == 'now': - value = time.time() - return email.utils.formatdate(value, localtime=localtime) - - -def get_now_date(): - return time.time() - - -def get_date(value, fmt): - if value == 'now': - value = time.time() - if '%' not in fmt: - suggest = php_format_to_strftime_format(fmt) - if suggest != fmt: - suggest_message = ("You probably want a format that looks " - "like: '%s'." % suggest) - else: - suggest_message = ("We can't suggest a proper date format " - "for you right now, though.") - raise Exception("Got incorrect date format: '%s\n" - "PieCrust 1 date formats won't work in PieCrust 2. " - "%s\n" - "Please check the `strftime` formatting page here: " - "https://docs.python.org/3/library/datetime.html" - "#strftime-and-strptime-behavior" % - (fmt, suggest_message)) - return time.strftime(fmt, time.localtime(value)) - - -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 - - -def php_format_to_strftime_format(fmt): - replacements = { - 'd': '%d', - 'D': '%a', - 'j': '%d', - 'l': '%A', - 'w': '%w', - 'z': '%j', - 'W': '%W', - 'F': '%B', - 'm': '%m', - 'M': '%b', - 'n': '%m', - 'y': '%Y', - 'Y': '%y', - 'g': '%I', - 'G': '%H', - 'h': '%I', - 'H': '%H', - 'i': '%M', - 's': '%S', - 'e': '%Z', - 'O': '%z', - 'c': '%Y-%m-%dT%H:%M:%SZ'} - return multi_replace(fmt, replacements) -