comparison 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
comparison
equal deleted inserted replaced
850:370e74941d32 851:2c7e57d80bba
1 from jinja2.ext import Extension, Markup
2 from jinja2.lexer import Token, describe_token
3 from jinja2.nodes import CallBlock, Const
4 from compressinja.html import HtmlCompressor, StreamProcessContext
5 from pygments import highlight
6 from pygments.formatters import HtmlFormatter
7 from pygments.lexers import get_lexer_by_name, guess_lexer
8 from piecrust.rendering import format_text
9
10
11 class PieCrustFormatExtension(Extension):
12 tags = set(['pcformat'])
13
14 def __init__(self, environment):
15 super(PieCrustFormatExtension, self).__init__(environment)
16
17 def parse(self, parser):
18 lineno = next(parser.stream).lineno
19 args = [parser.parse_expression()]
20 body = parser.parse_statements(['name:endpcformat'], drop_needle=True)
21 return CallBlock(self.call_method('_format', args),
22 [], [], body).set_lineno(lineno)
23
24 def _format(self, format_name, caller=None):
25 body = caller()
26 text = format_text(self.environment.app,
27 format_name,
28 Markup(body.rstrip()).unescape(),
29 exact_format=True)
30 return text
31
32
33 class PieCrustHighlightExtension(Extension):
34 tags = set(['highlight', 'geshi'])
35
36 def __init__(self, environment):
37 super(PieCrustHighlightExtension, self).__init__(environment)
38
39 def parse(self, parser):
40 lineno = next(parser.stream).lineno
41
42 # Extract the language name.
43 args = [parser.parse_expression()]
44
45 # Extract optional arguments.
46 kwarg_names = {'line_numbers': 0, 'use_classes': 0, 'class': 1,
47 'id': 1}
48 kwargs = {}
49 while not parser.stream.current.test('block_end'):
50 name = parser.stream.expect('name')
51 if name.value not in kwarg_names:
52 raise Exception("'%s' is not a valid argument for the code "
53 "highlighting tag." % name.value)
54 if kwarg_names[name.value] == 0:
55 kwargs[name.value] = Const(True)
56 elif parser.stream.skip_if('assign'):
57 kwargs[name.value] = parser.parse_expression()
58
59 # body of the block
60 body = parser.parse_statements(['name:endhighlight', 'name:endgeshi'],
61 drop_needle=True)
62
63 return CallBlock(self.call_method('_highlight', args, kwargs),
64 [], [], body).set_lineno(lineno)
65
66 def _highlight(self, lang, line_numbers=False, use_classes=False,
67 css_class=None, css_id=None, caller=None):
68 # Try to be mostly compatible with Jinja2-highlight's settings.
69 body = caller()
70
71 if lang is None:
72 lexer = guess_lexer(body)
73 else:
74 lexer = get_lexer_by_name(lang, stripall=False)
75
76 if css_class is None:
77 try:
78 css_class = self.environment.jinja2_highlight_cssclass
79 except AttributeError:
80 pass
81
82 if css_class is not None:
83 formatter = HtmlFormatter(cssclass=css_class,
84 linenos=line_numbers)
85 else:
86 formatter = HtmlFormatter(linenos=line_numbers)
87
88 code = highlight(Markup(body.rstrip()).unescape(), lexer, formatter)
89 return code
90
91
92 def get_highlight_css(style_name='default', class_name='.highlight'):
93 return HtmlFormatter(style=style_name).get_style_defs(class_name)
94
95
96 class PieCrustCacheExtension(Extension):
97 tags = set(['pccache', 'cache'])
98
99 def __init__(self, environment):
100 super(PieCrustCacheExtension, self).__init__(environment)
101 environment.extend(
102 piecrust_cache_prefix='',
103 piecrust_cache={}
104 )
105
106 def parse(self, parser):
107 # the first token is the token that started the tag. In our case
108 # we only listen to ``'pccache'`` so this will be a name token with
109 # `pccache` as value. We get the line number so that we can give
110 # that line number to the nodes we create by hand.
111 lineno = next(parser.stream).lineno
112
113 # now we parse a single expression that is used as cache key.
114 args = [parser.parse_expression()]
115
116 # now we parse the body of the cache block up to `endpccache` and
117 # drop the needle (which would always be `endpccache` in that case)
118 body = parser.parse_statements(['name:endpccache', 'name:endcache'],
119 drop_needle=True)
120
121 # now return a `CallBlock` node that calls our _cache_support
122 # helper method on this extension.
123 return CallBlock(self.call_method('_cache_support', args),
124 [], [], body).set_lineno(lineno)
125
126 def _cache_support(self, name, caller):
127 key = self.environment.piecrust_cache_prefix + name
128
129 exc_stack = self.environment.app.env.exec_info_stack
130 render_ctx = exc_stack.current_page_info.render_ctx
131 rdr_pass = render_ctx.current_pass_info
132
133 # try to load the block from the cache
134 # if there is no fragment in the cache, render it and store
135 # it in the cache.
136 pair = self.environment.piecrust_cache.get(key)
137 if pair is not None:
138 rdr_pass.used_source_names.update(pair[1])
139 return pair[0]
140
141 pair = self.environment.piecrust_cache.get(key)
142 if pair is not None:
143 rdr_pass.used_source_names.update(pair[1])
144 return pair[0]
145
146 prev_used = rdr_pass.used_source_names.copy()
147 rv = caller()
148 after_used = rdr_pass.used_source_names.copy()
149 used_delta = after_used.difference(prev_used)
150 self.environment.piecrust_cache[key] = (rv, used_delta)
151 return rv
152
153
154 class PieCrustSpacelessExtension(HtmlCompressor):
155 """ A re-implementation of `SelectiveHtmlCompressor` so that we can
156 both use `strip` or `spaceless` in templates.
157 """
158 def filter_stream(self, stream):
159 ctx = StreamProcessContext(stream)
160 strip_depth = 0
161 while 1:
162 if stream.current.type == 'block_begin':
163 for tk in ['strip', 'spaceless']:
164 change = self._processToken(ctx, stream, tk)
165 if change != 0:
166 strip_depth += change
167 if strip_depth < 0:
168 ctx.fail('Unexpected tag end%s' % tk)
169 break
170 if strip_depth > 0 and stream.current.type == 'data':
171 ctx.token = stream.current
172 value = self.normalize(ctx)
173 yield Token(stream.current.lineno, 'data', value)
174 else:
175 yield stream.current
176 next(stream)
177
178 def _processToken(self, ctx, stream, test_token):
179 change = 0
180 if (stream.look().test('name:%s' % test_token) or
181 stream.look().test('name:end%s' % test_token)):
182 stream.skip()
183 if stream.current.value == test_token:
184 change = 1
185 else:
186 change = -1
187 stream.skip()
188 if stream.current.type != 'block_end':
189 ctx.fail('expected end of block, got %s' %
190 describe_token(stream.current))
191 stream.skip()
192 return change