1from .compat import escape
2from .jsonify import encode
3
4_builtin_renderers = {}
5error_formatters = []
6
7#
8# JSON rendering engine
9#
10
11
12class JsonRenderer(object):
13    '''
14    Defines the builtin ``JSON`` renderer.
15    '''
16    def __init__(self, path, extra_vars):
17        pass
18
19    def render(self, template_path, namespace):
20        '''
21        Implements ``JSON`` rendering.
22        '''
23        return encode(namespace)
24
25    # TODO: add error formatter for json (pass it through json lint?)
26
27_builtin_renderers['json'] = JsonRenderer
28
29#
30# Genshi rendering engine
31#
32
33try:
34    from genshi.template import (TemplateLoader,
35                                 TemplateError as gTemplateError)
36
37    class GenshiRenderer(object):
38        '''
39        Defines the builtin ``Genshi`` renderer.
40        '''
41        def __init__(self, path, extra_vars):
42            self.loader = TemplateLoader([path], auto_reload=True)
43            self.extra_vars = extra_vars
44
45        def render(self, template_path, namespace):
46            '''
47            Implements ``Genshi`` rendering.
48            '''
49            tmpl = self.loader.load(template_path)
50            stream = tmpl.generate(**self.extra_vars.make_ns(namespace))
51            return stream.render('html')
52
53    _builtin_renderers['genshi'] = GenshiRenderer
54
55    def format_genshi_error(exc_value):
56        '''
57        Implements ``Genshi`` renderer error formatting.
58        '''
59        if isinstance(exc_value, (gTemplateError)):
60            retval = '<h4>Genshi error %s</h4>' % escape(
61                exc_value.args[0],
62                True
63            )
64            retval += format_line_context(exc_value.filename, exc_value.lineno)
65            return retval
66    error_formatters.append(format_genshi_error)
67except ImportError:                                 # pragma no cover
68    pass
69
70
71#
72# Mako rendering engine
73#
74
75try:
76    from mako.lookup import TemplateLookup
77    from mako.exceptions import (CompileException, SyntaxException,
78                                 html_error_template)
79
80    class MakoRenderer(object):
81        '''
82        Defines the builtin ``Mako`` renderer.
83        '''
84        def __init__(self, path, extra_vars):
85            self.loader = TemplateLookup(
86                directories=[path],
87                output_encoding='utf-8'
88            )
89            self.extra_vars = extra_vars
90
91        def render(self, template_path, namespace):
92            '''
93            Implements ``Mako`` rendering.
94            '''
95            tmpl = self.loader.get_template(template_path)
96            return tmpl.render(**self.extra_vars.make_ns(namespace))
97
98    _builtin_renderers['mako'] = MakoRenderer
99
100    def format_mako_error(exc_value):
101        '''
102        Implements ``Mako`` renderer error formatting.
103        '''
104        if isinstance(exc_value, (CompileException, SyntaxException)):
105            return html_error_template().render(full=False, css=False)
106
107    error_formatters.append(format_mako_error)
108except ImportError:                                 # pragma no cover
109    pass
110
111
112#
113# Kajiki rendering engine
114#
115
116try:
117    from kajiki.loader import FileLoader
118
119    class KajikiRenderer(object):
120        '''
121        Defines the builtin ``Kajiki`` renderer.
122        '''
123        def __init__(self, path, extra_vars):
124            self.loader = FileLoader(path, reload=True)
125            self.extra_vars = extra_vars
126
127        def render(self, template_path, namespace):
128            '''
129            Implements ``Kajiki`` rendering.
130            '''
131            Template = self.loader.import_(template_path)
132            stream = Template(self.extra_vars.make_ns(namespace))
133            return stream.render()
134    _builtin_renderers['kajiki'] = KajikiRenderer
135    # TODO: add error formatter for kajiki
136except ImportError:                                 # pragma no cover
137    pass
138
139#
140# Jinja2 rendering engine
141#
142try:
143    from jinja2 import Environment, FileSystemLoader
144    from jinja2.exceptions import TemplateSyntaxError as jTemplateSyntaxError
145
146    class JinjaRenderer(object):
147        '''
148        Defines the builtin ``Jinja`` renderer.
149        '''
150        def __init__(self, path, extra_vars):
151            self.env = Environment(loader=FileSystemLoader(path))
152            self.extra_vars = extra_vars
153
154        def render(self, template_path, namespace):
155            '''
156            Implements ``Jinja`` rendering.
157            '''
158            template = self.env.get_template(template_path)
159            return template.render(self.extra_vars.make_ns(namespace))
160    _builtin_renderers['jinja'] = JinjaRenderer
161
162    def format_jinja_error(exc_value):
163        '''
164        Implements ``Jinja`` renderer error formatting.
165        '''
166        retval = '<h4>Jinja2 error in \'%s\' on line %d</h4><div>%s</div>'
167        if isinstance(exc_value, (jTemplateSyntaxError)):
168            retval = retval % (
169                exc_value.name,
170                exc_value.lineno,
171                exc_value.message
172            )
173            retval += format_line_context(exc_value.filename, exc_value.lineno)
174            return retval
175    error_formatters.append(format_jinja_error)
176except ImportError:                                 # pragma no cover
177    pass
178
179
180#
181# format helper function
182#
183def format_line_context(filename, lineno, context=10):
184    '''
185    Formats the the line context for error rendering.
186
187    :param filename: the location of the file, within which the error occurred
188    :param lineno: the offending line number
189    :param context: number of lines of code to display before and after the
190                    offending line.
191    '''
192    with open(filename) as f:
193        lines = f.readlines()
194
195    lineno = lineno - 1  # files are indexed by 1 not 0
196    if lineno > 0:
197        start_lineno = max(lineno - context, 0)
198        end_lineno = lineno + context
199
200        lines = [escape(l, True) for l in lines[start_lineno:end_lineno]]
201        i = lineno - start_lineno
202        lines[i] = '<strong>%s</strong>' % lines[i]
203
204    else:
205        lines = [escape(l, True) for l in lines[:context]]
206    msg = '<pre style="background-color:#ccc;padding:2em;">%s</pre>'
207    return msg % ''.join(lines)
208
209
210#
211# Extra Vars Rendering
212#
213class ExtraNamespace(object):
214    '''
215    Extra variables for the template namespace to pass to the renderer as named
216    parameters.
217
218    :param extras: dictionary of extra parameters. Defaults to an empty dict.
219    '''
220    def __init__(self, extras={}):
221        self.namespace = dict(extras)
222
223    def update(self, d):
224        '''
225        Updates the extra variable dictionary for the namespace.
226        '''
227        self.namespace.update(d)
228
229    def make_ns(self, ns):
230        '''
231        Returns the `lazily` created template namespace.
232        '''
233        if self.namespace:
234            val = {}
235            val.update(self.namespace)
236            val.update(ns)
237            return val
238        else:
239            return ns
240
241
242#
243# Rendering Factory
244#
245class RendererFactory(object):
246    '''
247    Manufactures known Renderer objects.
248
249    :param custom_renderers: custom-defined renderers to manufacture
250    :param extra_vars: extra vars for the template namespace
251    '''
252    def __init__(self, custom_renderers={}, extra_vars={}):
253        self._renderers = {}
254        self._renderer_classes = dict(_builtin_renderers)
255        self.add_renderers(custom_renderers)
256        self.extra_vars = ExtraNamespace(extra_vars)
257
258    def add_renderers(self, custom_dict):
259        '''
260        Adds a custom renderer.
261
262        :param custom_dict: a dictionary of custom renderers to add
263        '''
264        self._renderer_classes.update(custom_dict)
265
266    def available(self, name):
267        '''
268        Returns true if queried renderer class is available.
269
270        :param name: renderer name
271        '''
272        return name in self._renderer_classes
273
274    def get(self, name, template_path):
275        '''
276        Returns the renderer object.
277
278        :param name: name of the requested renderer
279        :param template_path: path to the template
280        '''
281        if name not in self._renderers:
282            cls = self._renderer_classes.get(name)
283            if cls is None:
284                return None
285            else:
286                self._renderers[name] = cls(template_path, self.extra_vars)
287        return self._renderers[name]
288
289    def keys(self, *args, **kwargs):
290        return self._renderer_classes.keys(*args, **kwargs)
291