Mercurial > piecrust2
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 |