summaryrefslogtreecommitdiff
path: root/src/zope/pagetemplate/pagetemplate.py
blob: 9c05c893cced634e69481cbdaec98d786e4edcb3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Page Template module

HTML- and XML-based template objects using TAL, TALES, and METAL.
"""
import sys
import six
from zope.tal.talparser import TALParser
from zope.tal.htmltalparser import HTMLTALParser
from zope.tal.talgenerator import TALGenerator
from zope.tal.talinterpreter import TALInterpreter
from zope.tales.engine import Engine
from zope.component import queryUtility

from zope.pagetemplate.interfaces import IPageTemplateSubclassing
from zope.pagetemplate.interfaces import IPageTemplateEngine
from zope.pagetemplate.interfaces import IPageTemplateProgram
from zope.interface import implementer
from zope.interface import provider

_default_options = {}


class StringIO(list):
    """Unicode aware append-only version of StringIO.
    """
    write = list.append

    def __init__(self, value=None):
        list.__init__(self)
        if value is not None:
            self.append(value)

    def getvalue(self):
        return u''.join(self)


@implementer(IPageTemplateSubclassing)
class PageTemplate(object):
    """Page Templates using TAL, TALES, and METAL.

    Subclassing
    -----------

    The following methods have certain internal responsibilities.

    pt_getContext(**keywords)
        Should ignore keyword arguments that it doesn't care about,
        and construct the namespace passed to the TALES expression
        engine.  This method is free to use the keyword arguments it
        receives.

    pt_render(namespace, source=False, sourceAnnotations=False, showtal=False)
        Responsible the TAL interpreter to perform the rendering.  The
        namespace argument is a mapping which defines the top-level
        namespaces passed to the TALES expression engine.

    __call__(*args, **keywords)
        Calls pt_getContext() to construct the top-level namespace
        passed to the TALES expression engine, then calls pt_render()
        to perform the rendering.
    """

    _error_start = '<!-- Page Template Diagnostics'
    _error_end = '-->'
    _newline = '\n'

    content_type = 'text/html'
    expand = 1
    _v_errors = ()
    _v_cooked = 0
    _v_macros = None
    _v_program = None
    _text = ''

    @property
    def macros(self):
        self._cook_check()
        return self._v_macros

    def pt_edit(self, text, content_type):
        if content_type:
            self.content_type = str(content_type)
        if hasattr(text, 'read'):
            text = text.read()
        self.write(text)

    def pt_getContext(self, args=(), options=_default_options, **ignored):
        rval = {'template': self,
                'options': options,
                'args': args,
                'nothing': None,
        }
        rval.update(self.pt_getEngine().getBaseNames())
        return rval

    def __call__(self, *args, **kwargs):
        return self.pt_render(self.pt_getContext(args, kwargs))

    def pt_getEngineContext(self, namespace):
        return self.pt_getEngine().getContext(namespace)

    def pt_getEngine(self):
        return Engine

    def pt_render(self, namespace, source=False, sourceAnnotations=False,
                  showtal=False):
        """Render this Page Template"""
        self._cook_check()

        __traceback_supplement__ = (
            PageTemplateTracebackSupplement, self, namespace
            )

        if self._v_errors:
            raise PTRuntimeError(str(self._v_errors))

        context = self.pt_getEngineContext(namespace)

        return self._v_program(
            context, self._v_macros, tal=not source, showtal=showtal,
            strictinsert=0, sourceAnnotations=sourceAnnotations
            )

    def pt_errors(self, namespace, check_macro_expansion=True):
        self._cook_check()
        err = self._v_errors
        if err:
            return err
        if check_macro_expansion:
            try:
                self.pt_render(namespace, source=1)
            except Exception:
                return ('Macro expansion failed', '%s: %s' % sys.exc_info()[:2])

    def _convert(self, string, text):
        """Adjust the string type to the type of text"""
        if isinstance(text, six.binary_type) and not isinstance(string, six.binary_type):
            return string.encode('utf-8')

        if isinstance(text, six.text_type) and not isinstance(string, six.text_type):
            return string.decode('utf-8')

        return string

    def write(self, text):
        # We accept both, since the text can either come from a file (and the
        # parser will take care of the encoding) or from a TTW template, in
        # which case we already have unicode.
        assert isinstance(text, (six.string_types, six.binary_type))

        def bs(s):
            """Bytes or str"""
            return self._convert(s, text)

        if text.startswith(bs(self._error_start)):
            errend = text.find(bs(self._error_end))
            if errend >= 0:
                text = text[errend + 3:]
                if text[:1] == bs(self._newline):
                    text = text[1:]
        if self._text != text:
            self._text = text

        # Always cook on an update, even if the source is the same;
        # the content-type might have changed.
        self._cook()

    def read(self, request=None):
        """Gets the source, sometimes with macros expanded."""
        self._cook_check()
        def bs(s):
            """Bytes or str"""
            return self._convert(s, self._text)
        if not self._v_errors:
            if not self.expand:
                return self._text
            try:
                # This gets called, if macro expansion is turned on.
                # Note that an empty dictionary is fine for the context at
                # this point, since we are not evaluating the template.
                context = self.pt_getContext(self, request)
                return self.pt_render(context, source=1)
            except:
                return (bs('%s\n Macro expansion failed\n %s\n-->\n' %
                           (self._error_start, "%s: %s" % sys.exc_info()[:2])) +
                        self._text)

        return bs('%s\n %s\n-->\n' % (self._error_start,
                                      '\n'.join(self._v_errors))) + \
               self._text

    def pt_source_file(self):
        """To be overridden."""
        return None

    def _cook_check(self):
        if not self._v_cooked:
            self._cook()

    def _cook(self):
        """Compile the TAL and METAL statments.

        Cooking must not fail due to compilation errors in templates.
        """

        pt_engine = self.pt_getEngine()
        source_file = self.pt_source_file()

        self._v_errors = ()

        try:
            engine = queryUtility(
                IPageTemplateEngine, default=PageTemplateEngine
                )
            self._v_program, self._v_macros = engine.cook(
                source_file, self._text, pt_engine, self.content_type)
        except:
            etype, e = sys.exc_info()[:2]
            self._v_errors = [
                "Compilation failed",
                "%s.%s: %s" % (etype.__module__, etype.__name__, e)
                ]

        self._v_cooked = 1


class PTRuntimeError(RuntimeError):
    '''The Page Template has template errors that prevent it from rendering.'''
    pass


@implementer(IPageTemplateProgram)
@provider(IPageTemplateEngine)
class PageTemplateEngine(object):
    """Page template engine that uses the TAL interpreter to render."""


    def __init__(self, program):
        self.program = program

    def __call__(self, context, macros, **options):
        output = StringIO(u'')
        interpreter = TALInterpreter(
            self.program, macros, context,
            stream=output, **options
            )
        interpreter()
        return output.getvalue()

    @classmethod
    def cook(cls, source_file, text, engine, content_type):
        if content_type == 'text/html':
            gen = TALGenerator(engine, xml=0, source_file=source_file)
            parser = HTMLTALParser(gen)
        else:
            gen = TALGenerator(engine, source_file=source_file)
            parser = TALParser(gen)

        parser.parseString(text)
        program, macros = parser.getCode()

        return cls(program), macros


#@implementer(ITracebackSupplement)
class PageTemplateTracebackSupplement(object):

    def __init__(self, pt, namespace):
        self.manageable_object = pt
        self.warnings = []
        try:
            e = pt.pt_errors(namespace, check_macro_expansion=False)
        except TypeError:
            # Old page template.
            e = pt.pt_errors(namespace)
        if e:
            self.warnings.extend(e)