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