1# Copyright 2019 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import sys
6
7import mako.runtime
8import mako.template
9import mako.util
10
11_MAKO_TEMPLATE_PASS_KEY = object()
12
13
14class MakoTemplate(object):
15    """Represents a compiled template object."""
16
17    _mako_template_cache = {}
18
19    def __init__(self, template_text):
20        assert isinstance(template_text, str)
21
22        template_params = {
23            "strict_undefined": True,
24        }
25
26        template = self._mako_template_cache.get(template_text)
27        if template is None:
28            template = mako.template.Template(
29                text=template_text, **template_params)
30            self._mako_template_cache[template_text] = template
31        self._template = template
32
33    def mako_template(self, pass_key=None):
34        assert pass_key is _MAKO_TEMPLATE_PASS_KEY
35        return self._template
36
37
38class MakoRenderer(object):
39    """Represents a renderer object implemented with Mako templates."""
40
41    def __init__(self):
42        self._text_buffer = None
43        self._is_invalidated = False
44        self._caller_stack = []
45        self._caller_stack_on_error = []
46
47    def reset(self):
48        """
49        Resets the rendering states of this object.  Must be called before
50        the first call to |render| or |render_text|.
51        """
52        self._text_buffer = mako.util.FastEncodingBuffer()
53        self._is_invalidated = False
54
55    def is_rendering_complete(self):
56        return not (self._is_invalidated or self._text_buffer is None
57                    or self._caller_stack)
58
59    def invalidate_rendering_result(self):
60        self._is_invalidated = True
61
62    def to_text(self):
63        """Returns the rendering result."""
64        assert self._text_buffer is not None
65        return self._text_buffer.getvalue()
66
67    def render(self, caller, template, template_vars):
68        """
69        Renders the template with variable bindings.
70
71        It's okay to invoke |render| method recursively and |caller| is pushed
72        onto the call stack, which is accessible via
73        |callers_from_first_to_last| method, etc.
74
75        Args:
76            caller: An object to be pushed onto the call stack.
77            template: A MakoTemplate.
78            template_vars: A dict of template variable bindings.
79        """
80        assert caller is not None
81        assert isinstance(template, MakoTemplate)
82        assert isinstance(template_vars, dict)
83
84        self._caller_stack.append(caller)
85
86        try:
87            mako_template = template.mako_template(
88                pass_key=_MAKO_TEMPLATE_PASS_KEY)
89            mako_context = mako.runtime.Context(self._text_buffer,
90                                                **template_vars)
91            mako_template.render_context(mako_context)
92        except:
93            # Print stacktrace of template rendering.
94            sys.stderr.write("\n")
95            sys.stderr.write("==== template rendering error ====\n")
96            sys.stderr.write("  * name: {}, type: {}\n".format(
97                _guess_caller_name(self.last_caller), type(self.last_caller)))
98            sys.stderr.write("  * depth: {}, module_id: {}\n".format(
99                len(self._caller_stack), mako_template.module_id))
100            sys.stderr.write("---- template source ----\n")
101            sys.stderr.write(mako_template.source)
102
103            # Save the error state at the deepest call.
104            current = self._caller_stack
105            on_error = self._caller_stack_on_error
106            if (len(current) <= len(on_error)
107                    and all(current[i] == on_error[i]
108                            for i in xrange(len(current)))):
109                pass  # Error happened in a deeper caller.
110            else:
111                self._caller_stack_on_error = list(self._caller_stack)
112
113            raise
114        finally:
115            self._caller_stack.pop()
116
117    def render_text(self, text):
118        """Renders a plain text as is."""
119        assert isinstance(text, str)
120        self._text_buffer.write(text)
121
122    def push_caller(self, caller):
123        self._caller_stack.append(caller)
124
125    def pop_caller(self):
126        self._caller_stack.pop()
127
128    @property
129    def callers_from_first_to_last(self):
130        """
131        Returns the callers of this renderer in the order from the first caller
132        to the last caller.
133        """
134        return iter(self._caller_stack)
135
136    @property
137    def callers_from_last_to_first(self):
138        """
139        Returns the callers of this renderer in the order from the last caller
140        to the first caller.
141        """
142        return reversed(self._caller_stack)
143
144    @property
145    def last_caller(self):
146        """Returns the last caller in the call stack of this renderer."""
147        return self._caller_stack[-1]
148
149    @property
150    def callers_on_error(self):
151        """
152        Returns the callers of this renderer in the order from the last caller
153        to the first caller at the moment when an exception was thrown.
154        """
155        return reversed(self._caller_stack_on_error)
156
157    @property
158    def last_caller_on_error(self):
159        """
160        Returns the deepest caller at the moment when an exception was thrown.
161        """
162        return self._caller_stack_on_error[-1]
163
164
165def _guess_caller_name(caller):
166    """Returns the best-guessed name of |caller|."""
167    try:
168        # Outer CodeNode may have a binding to the caller.
169        for name, value in caller.outer.template_vars.items():
170            if value is caller:
171                return name
172        try:
173            # Outer ListNode may contain the caller.
174            for index, value in enumerate(caller.outer, 1):
175                if value is caller:
176                    return "{}-of-{}-in-list".format(index, len(caller.outer))
177        except:
178            pass
179        return "<no name>"
180    except:
181        return "<unknown>"
182