Mercurial > piecrust2
comparison piecrust/templating/jinjaengine.py @ 3:f485ba500df3
Gigantic change to basically make PieCrust 2 vaguely functional.
- Serving works, with debug window.
- Baking works, multi-threading, with dependency handling.
- Various things not implemented yet.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sun, 10 Aug 2014 23:43:16 -0700 |
parents | |
children | 474c9882decf |
comparison
equal
deleted
inserted
replaced
2:40fa08b261b9 | 3:f485ba500df3 |
---|---|
1 import re | |
2 import time | |
3 import logging | |
4 import strict_rfc3339 | |
5 from jinja2 import Environment, FileSystemLoader, TemplateNotFound | |
6 from jinja2.exceptions import TemplateSyntaxError | |
7 from jinja2.ext import Extension, Markup | |
8 from jinja2.nodes import CallBlock, Const | |
9 from pygments import highlight | |
10 from pygments.formatters import HtmlFormatter | |
11 from pygments.lexers import get_lexer_by_name, guess_lexer | |
12 from piecrust.rendering import format_text | |
13 from piecrust.routing import CompositeRouteFunction | |
14 from piecrust.templating.base import TemplateEngine, TemplateNotFoundError | |
15 | |
16 | |
17 logger = logging.getLogger(__name__) | |
18 | |
19 | |
20 class JinjaTemplateEngine(TemplateEngine): | |
21 # Name `twig` is for backwards compatibility with PieCrust 1.x. | |
22 ENGINE_NAMES = ['jinja', 'jinja2', 'twig'] | |
23 EXTENSIONS = ['jinja', 'jinja2', 'twig'] | |
24 | |
25 def __init__(self): | |
26 self.env = None | |
27 | |
28 def renderString(self, txt, data, filename=None, line_offset=0): | |
29 self._ensureLoaded() | |
30 tpl = self.env.from_string(txt) | |
31 try: | |
32 return tpl.render(data) | |
33 except TemplateSyntaxError as tse: | |
34 tse.lineno += line_offset | |
35 if filename: | |
36 tse.filename = filename | |
37 import sys | |
38 _, __, traceback = sys.exc_info() | |
39 raise tse, None, traceback | |
40 | |
41 def renderFile(self, paths, data): | |
42 self._ensureLoaded() | |
43 tpl = None | |
44 logger.debug("Looking for template: %s" % paths) | |
45 for p in paths: | |
46 try: | |
47 tpl = self.env.get_template(p) | |
48 break | |
49 except TemplateNotFound: | |
50 pass | |
51 if tpl is None: | |
52 raise TemplateNotFoundError() | |
53 return tpl.render(data) | |
54 | |
55 | |
56 def _ensureLoaded(self): | |
57 if self.env: | |
58 return | |
59 loader = FileSystemLoader(self.app.templates_dirs) | |
60 self.env = PieCrustEnvironment( | |
61 self.app, | |
62 loader=loader, | |
63 extensions=['jinja2.ext.autoescape', | |
64 PieCrustHighlightExtension, | |
65 PieCrustCacheExtension]) | |
66 | |
67 | |
68 class PieCrustEnvironment(Environment): | |
69 def __init__(self, app, *args, **kwargs): | |
70 super(PieCrustEnvironment, self).__init__(*args, **kwargs) | |
71 self.app = app | |
72 self.globals.update({ | |
73 'fail': raise_exception}) | |
74 self.filters.update({ | |
75 'formatwith': self._formatWith, | |
76 'markdown': lambda v: self._formatWith(v, 'markdown'), | |
77 'textile': lambda v: self._formatWith(v, 'textile'), | |
78 'nocache': add_no_cache_parameter, | |
79 'wordcount': get_word_count, | |
80 'stripoutertag': strip_outer_tag, | |
81 'stripslash': strip_slash, | |
82 'titlecase': title_case, | |
83 'atomdate': get_atom_date, | |
84 'date': get_date}) | |
85 # Backwards compatibility with PieCrust 1.x. | |
86 self.globals.update({ | |
87 'pcfail': raise_exception}) | |
88 | |
89 # Backwards compatibility with Twig. | |
90 twig_compatibility_mode = app.config.get('jinja/twig_compatibility') | |
91 if twig_compatibility_mode is None or twig_compatibility_mode is True: | |
92 self.trim_blocks = True | |
93 self.filters['raw'] = self.filters['safe'] | |
94 | |
95 # Add route functions. | |
96 for route in app.routes: | |
97 name = route.template_func_name | |
98 func = self.globals.get(name) | |
99 if func is None: | |
100 func = CompositeRouteFunction() | |
101 func.addFunc(route) | |
102 self.globals[name] = func | |
103 elif isinstance(func, CompositeRouteFunction): | |
104 self.globals[name].addFunc(route) | |
105 else: | |
106 raise Exception("Route function '%s' collides with an " | |
107 "existing function or template data." % | |
108 name) | |
109 | |
110 def _formatWith(self, value, format_name): | |
111 return format_text(self.app, format_name, value) | |
112 | |
113 | |
114 def raise_exception(msg): | |
115 raise Exception(msg) | |
116 | |
117 | |
118 def add_no_cache_parameter(value, param_name='t', param_value=None): | |
119 if not param_value: | |
120 param_value = time.time() | |
121 if '?' in value: | |
122 value += '&' | |
123 else: | |
124 value += '?' | |
125 value += '%s=%s' % (param_name, param_value) | |
126 return value | |
127 | |
128 | |
129 def get_word_count(value): | |
130 return len(value.split()) | |
131 | |
132 | |
133 def strip_outer_tag(value, tag=None): | |
134 tag_pattern = '[a-z]+[a-z0-9]*' | |
135 if tag is not None: | |
136 tag_pattern = re.escape(tag) | |
137 pat = r'^\<' + tag_pattern + r'\>(.*)\</' + tag_pattern + '>$' | |
138 m = re.match(pat, value) | |
139 if m: | |
140 return m.group(1) | |
141 return value | |
142 | |
143 | |
144 def strip_slash(value): | |
145 return value.rstrip('/') | |
146 | |
147 | |
148 def title_case(value): | |
149 return value.title() | |
150 | |
151 | |
152 def get_atom_date(value): | |
153 return strict_rfc3339.timestamp_to_rfc3339_localoffset(int(value)) | |
154 | |
155 | |
156 def get_date(value, fmt): | |
157 return time.strftime(fmt, time.localtime(value)) | |
158 | |
159 | |
160 class PieCrustHighlightExtension(Extension): | |
161 tags = set(['highlight', 'geshi']) | |
162 | |
163 def __init__(self, environment): | |
164 super(PieCrustHighlightExtension, self).__init__(environment) | |
165 | |
166 def parse(self, parser): | |
167 lineno = next(parser.stream).lineno | |
168 | |
169 # Extract the language name. | |
170 args = [parser.parse_expression()] | |
171 | |
172 # Extract optional arguments. | |
173 kwarg_names = {'line_numbers': 0, 'use_classes': 0, 'class': 1, 'id': 1} | |
174 kwargs = {} | |
175 while not parser.stream.current.test('block_end'): | |
176 name = parser.stream.expect('name') | |
177 if name.value not in kwarg_names: | |
178 raise Exception("'%s' is not a valid argument for the code " | |
179 "highlighting tag." % name.value) | |
180 if kwarg_names[name.value] == 0: | |
181 kwargs[name.value] = Const(True) | |
182 elif parser.stream.skip_if('assign'): | |
183 kwargs[name.value] = parser.parse_expression() | |
184 | |
185 # body of the block | |
186 body = parser.parse_statements(['name:endhighlight', 'name:endgeshi'], | |
187 drop_needle=True) | |
188 | |
189 return CallBlock(self.call_method('_highlight', args, kwargs), | |
190 [], [], body).set_lineno(lineno) | |
191 | |
192 def _highlight(self, lang, line_numbers=False, use_classes=False, | |
193 css_class=None, css_id=None, caller=None): | |
194 # Try to be mostly compatible with Jinja2-highlight's settings. | |
195 body = caller() | |
196 | |
197 if lang is None: | |
198 lexer = guess_lexer(body) | |
199 else: | |
200 lexer = get_lexer_by_name(lang, stripall=False) | |
201 | |
202 if css_class is None: | |
203 try: | |
204 css_class = self.environment.jinja2_highlight_cssclass | |
205 except AttributeError: | |
206 pass | |
207 | |
208 if css_class is not None: | |
209 formatter = HtmlFormatter(cssclass=css_class, | |
210 linenos=line_numbers) | |
211 else: | |
212 formatter = HtmlFormatter(linenos=line_numbers) | |
213 | |
214 code = highlight(Markup(body.rstrip()).unescape(), lexer, formatter) | |
215 return code | |
216 | |
217 | |
218 class PieCrustCacheExtension(Extension): | |
219 tags = set(['pccache']) | |
220 | |
221 def __init__(self, environment): | |
222 super(PieCrustCacheExtension, self).__init__(environment) | |
223 | |
224 environment.extend( | |
225 piecrust_cache_prefix='', | |
226 piecrust_cache={} | |
227 ) | |
228 | |
229 def parse(self, parser): | |
230 # the first token is the token that started the tag. In our case | |
231 # we only listen to ``'pccache'`` so this will be a name token with | |
232 # `pccache` as value. We get the line number so that we can give | |
233 # that line number to the nodes we create by hand. | |
234 lineno = parser.stream.next().lineno | |
235 | |
236 # now we parse a single expression that is used as cache key. | |
237 args = [parser.parse_expression()] | |
238 | |
239 # now we parse the body of the cache block up to `endpccache` and | |
240 # drop the needle (which would always be `endpccache` in that case) | |
241 body = parser.parse_statements(['name:endpccache'], drop_needle=True) | |
242 | |
243 # now return a `CallBlock` node that calls our _cache_support | |
244 # helper method on this extension. | |
245 return CallBlock(self.call_method('_cache_support', args), | |
246 [], [], body).set_lineno(lineno) | |
247 | |
248 def _cache_support(self, name, caller): | |
249 key = self.environment.piecrust_cache_prefix + name | |
250 | |
251 # try to load the block from the cache | |
252 # if there is no fragment in the cache, render it and store | |
253 # it in the cache. | |
254 rv = self.environment.piecrust_cache.get(key) | |
255 if rv is not None: | |
256 return rv | |
257 rv = caller() | |
258 self.environment.piecrust_cache[key] = rv | |
259 return rv | |
260 |