1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 """provides simpleTAL extensions for CubicWeb |
|
19 |
|
20 """ |
|
21 |
|
22 __docformat__ = "restructuredtext en" |
|
23 |
|
24 import sys |
|
25 import re |
|
26 from os.path import exists, isdir, join |
|
27 from logging import getLogger |
|
28 from StringIO import StringIO |
|
29 |
|
30 from simpletal import simpleTAL, simpleTALES |
|
31 |
|
32 from logilab.common.decorators import cached |
|
33 |
|
34 LOGGER = getLogger('cubicweb.tal') |
|
35 |
|
36 |
|
37 class LoggerAdapter(object): |
|
38 def __init__(self, tal_logger): |
|
39 self.tal_logger = tal_logger |
|
40 |
|
41 def debug(self, msg): |
|
42 LOGGER.debug(msg) |
|
43 |
|
44 def warn(self, msg): |
|
45 LOGGER.warning(msg) |
|
46 |
|
47 def __getattr__(self, attrname): |
|
48 return getattr(self.tal_logger, attrname) |
|
49 |
|
50 |
|
51 class CubicWebContext(simpleTALES.Context): |
|
52 """add facilities to access entity / resultset""" |
|
53 |
|
54 def __init__(self, options=None, allowPythonPath=1): |
|
55 simpleTALES.Context.__init__(self, options, allowPythonPath) |
|
56 self.log = LoggerAdapter(self.log) |
|
57 |
|
58 def update(self, context): |
|
59 for varname, value in context.items(): |
|
60 self.addGlobal(varname, value) |
|
61 |
|
62 def addRepeat(self, name, var, initialValue): |
|
63 simpleTALES.Context.addRepeat(self, name, var, initialValue) |
|
64 |
|
65 # XXX FIXME need to find a clean to define OPCODE values for extensions |
|
66 I18N_CONTENT = 18 |
|
67 I18N_REPLACE = 19 |
|
68 RQL_EXECUTE = 20 |
|
69 # simpleTAL uses the OPCODE values to define priority over commands. |
|
70 # TAL_ITER should have the same priority than TAL_REPEAT (i.e. 3), but |
|
71 # we can't use the same OPCODE for two different commands without changing |
|
72 # the simpleTAL implementation. Another solution would be to totally override |
|
73 # the REPEAT implementation with the ITER one, but some specific operations |
|
74 # (involving len() for instance) are not implemented for ITER, so we prefer |
|
75 # to keep both implementations for now, and to fool simpleTAL by using a float |
|
76 # number between 3 and 4 |
|
77 TAL_ITER = 3.1 |
|
78 |
|
79 |
|
80 # FIX simpleTAL HTML 4.01 stupidity |
|
81 # (simpleTAL never closes tags like INPUT, IMG, HR ...) |
|
82 simpleTAL.HTML_FORBIDDEN_ENDTAG.clear() |
|
83 |
|
84 class CubicWebTemplateCompiler(simpleTAL.HTMLTemplateCompiler): |
|
85 """extends default compiler by adding i18n:content commands""" |
|
86 |
|
87 def __init__(self): |
|
88 simpleTAL.HTMLTemplateCompiler.__init__(self) |
|
89 self.commandHandler[I18N_CONTENT] = self.compile_cmd_i18n_content |
|
90 self.commandHandler[I18N_REPLACE] = self.compile_cmd_i18n_replace |
|
91 self.commandHandler[RQL_EXECUTE] = self.compile_cmd_rql |
|
92 self.commandHandler[TAL_ITER] = self.compile_cmd_tal_iter |
|
93 |
|
94 def setTALPrefix(self, prefix): |
|
95 simpleTAL.TemplateCompiler.setTALPrefix(self, prefix) |
|
96 self.tal_attribute_map['i18n:content'] = I18N_CONTENT |
|
97 self.tal_attribute_map['i18n:replace'] = I18N_REPLACE |
|
98 self.tal_attribute_map['rql:execute'] = RQL_EXECUTE |
|
99 self.tal_attribute_map['tal:iter'] = TAL_ITER |
|
100 |
|
101 def compile_cmd_i18n_content(self, argument): |
|
102 # XXX tal:content structure=, text= should we support this ? |
|
103 structure_flag = 0 |
|
104 return (I18N_CONTENT, (argument, False, structure_flag, self.endTagSymbol)) |
|
105 |
|
106 def compile_cmd_i18n_replace(self, argument): |
|
107 # XXX tal:content structure=, text= should we support this ? |
|
108 structure_flag = 0 |
|
109 return (I18N_CONTENT, (argument, True, structure_flag, self.endTagSymbol)) |
|
110 |
|
111 def compile_cmd_rql(self, argument): |
|
112 return (RQL_EXECUTE, (argument, self.endTagSymbol)) |
|
113 |
|
114 def compile_cmd_tal_iter(self, argument): |
|
115 original_id, (var_name, expression, end_tag_symbol) = \ |
|
116 simpleTAL.HTMLTemplateCompiler.compileCmdRepeat(self, argument) |
|
117 return (TAL_ITER, (var_name, expression, self.endTagSymbol)) |
|
118 |
|
119 def getTemplate(self): |
|
120 return CubicWebTemplate(self.commandList, self.macroMap, self.symbolLocationTable) |
|
121 |
|
122 def compileCmdAttributes (self, argument): |
|
123 """XXX modified to support single attribute |
|
124 definition ending by a ';' |
|
125 |
|
126 backport this to simpleTAL |
|
127 """ |
|
128 # Compile tal:attributes into attribute command |
|
129 # Argument: [(attributeName, expression)] |
|
130 |
|
131 # Break up the list of attribute settings first |
|
132 commandArgs = [] |
|
133 # We only want to match semi-colons that are not escaped |
|
134 argumentSplitter = re.compile(r'(?<!;);(?!;)') |
|
135 for attributeStmt in argumentSplitter.split(argument): |
|
136 if not attributeStmt.strip(): |
|
137 continue |
|
138 # remove any leading space and un-escape any semi-colons |
|
139 attributeStmt = attributeStmt.lstrip().replace(';;', ';') |
|
140 # Break each attributeStmt into name and expression |
|
141 stmtBits = attributeStmt.split(' ') |
|
142 if (len (stmtBits) < 2): |
|
143 # Error, badly formed attributes command |
|
144 msg = "Badly formed attributes command '%s'. Attributes commands must be of the form: 'name expression[;name expression]'" % argument |
|
145 self.log.error(msg) |
|
146 raise simpleTAL.TemplateParseException(self.tagAsText(self.currentStartTag), msg) |
|
147 attName = stmtBits[0] |
|
148 attExpr = " ".join(stmtBits[1:]) |
|
149 commandArgs.append((attName, attExpr)) |
|
150 return (simpleTAL.TAL_ATTRIBUTES, commandArgs) |
|
151 |
|
152 |
|
153 class CubicWebTemplateInterpreter(simpleTAL.TemplateInterpreter): |
|
154 """provides implementation for interpreting cubicweb extensions""" |
|
155 def __init__(self): |
|
156 simpleTAL.TemplateInterpreter.__init__(self) |
|
157 self.commandHandler[I18N_CONTENT] = self.cmd_i18n |
|
158 self.commandHandler[TAL_ITER] = self.cmdRepeat |
|
159 # self.commandHandler[RQL_EXECUTE] = self.cmd_rql |
|
160 |
|
161 def cmd_i18n(self, command, args): |
|
162 """i18n:content and i18n:replace implementation""" |
|
163 string, replace_flag, structure_flag, end_symbol = args |
|
164 if replace_flag: |
|
165 self.outputTag = 0 |
|
166 result = self.context.globals['_'](string) |
|
167 self.tagContent = (0, result) |
|
168 self.movePCForward = self.symbolTable[end_symbol] |
|
169 self.programCounter += 1 |
|
170 |
|
171 |
|
172 class CubicWebTemplate(simpleTAL.HTMLTemplate): |
|
173 """overrides HTMLTemplate.expand() to systematically use CubicWebInterpreter |
|
174 """ |
|
175 def expand(self, context, outputFile): |
|
176 interpreter = CubicWebTemplateInterpreter() |
|
177 interpreter.initialise(context, outputFile) |
|
178 simpleTAL.HTMLTemplate.expand(self, context, outputFile,# outputEncoding='unicode', |
|
179 interpreter=interpreter) |
|
180 |
|
181 def expandInline(self, context, outputFile, interpreter): |
|
182 """ Internally used when expanding a template that is part of a context.""" |
|
183 try: |
|
184 interpreter.execute(self) |
|
185 except UnicodeError as unierror: |
|
186 LOGGER.exception(str(unierror)) |
|
187 exc = simpleTALES.ContextContentException( |
|
188 "found non-unicode %r string in Context!" % unierror.args[1]) |
|
189 exc.__traceback__ = sys.exc_info()[-1] |
|
190 raise exc |
|
191 |
|
192 |
|
193 def compile_template(template): |
|
194 """compiles a TAL template string |
|
195 :type template: unicode |
|
196 :param template: a TAL-compliant template string |
|
197 """ |
|
198 string_buffer = StringIO(template) |
|
199 compiler = CubicWebTemplateCompiler() |
|
200 compiler.parseTemplate(string_buffer) # , inputEncoding='unicode') |
|
201 return compiler.getTemplate() |
|
202 |
|
203 |
|
204 def compile_template_file(filepath): |
|
205 """compiles a TAL template file |
|
206 :type filepath: str |
|
207 :param template: path of the file to compile |
|
208 """ |
|
209 fp = open(filepath) |
|
210 file_content = unicode(fp.read()) # template file should be pure ASCII |
|
211 fp.close() |
|
212 return compile_template(file_content) |
|
213 |
|
214 |
|
215 def evaluatePython (self, expr): |
|
216 if not self.allowPythonPath: |
|
217 return self.false |
|
218 globals = {} |
|
219 for name, value in self.globals.items(): |
|
220 if isinstance (value, simpleTALES.ContextVariable): |
|
221 value = value.rawValue() |
|
222 globals[name] = value |
|
223 globals['path'] = self.pythonPathFuncs.path |
|
224 globals['string'] = self.pythonPathFuncs.string |
|
225 globals['exists'] = self.pythonPathFuncs.exists |
|
226 globals['nocall'] = self.pythonPathFuncs.nocall |
|
227 globals['test'] = self.pythonPathFuncs.test |
|
228 locals = {} |
|
229 for name, value in self.locals.items(): |
|
230 if (isinstance (value, simpleTALES.ContextVariable)): |
|
231 value = value.rawValue() |
|
232 locals[name] = value |
|
233 # XXX precompile expr will avoid late syntax error |
|
234 try: |
|
235 result = eval(expr, globals, locals) |
|
236 except Exception as ex: |
|
237 ex = ex.__class__('in %r: %s' % (expr, ex)) |
|
238 ex.__traceback__ = sys.exc_info()[-1] |
|
239 raise ex |
|
240 if (isinstance (result, simpleTALES.ContextVariable)): |
|
241 return result.value() |
|
242 return result |
|
243 |
|
244 simpleTALES.Context.evaluatePython = evaluatePython |
|
245 |
|
246 |
|
247 class talbased(object): |
|
248 def __init__(self, filename, write=True): |
|
249 ## if not osp.isfile(filepath): |
|
250 ## # print "[tal.py] just for tests..." |
|
251 ## # get parent frame |
|
252 ## directory = osp.abspath(osp.dirname(sys._getframe(1).f_globals['__file__'])) |
|
253 ## filepath = osp.join(directory, filepath) |
|
254 self.filename = filename |
|
255 self.write = write |
|
256 |
|
257 def __call__(self, viewfunc): |
|
258 def wrapped(instance, *args, **kwargs): |
|
259 variables = viewfunc(instance, *args, **kwargs) |
|
260 html = instance.tal_render(self._compiled_template(instance), variables) |
|
261 if self.write: |
|
262 instance.w(html) |
|
263 else: |
|
264 return html |
|
265 return wrapped |
|
266 |
|
267 def _compiled_template(self, instance): |
|
268 for fileordirectory in instance.config.appobjects_path(): |
|
269 filepath = join(fileordirectory, self.filename) |
|
270 if isdir(fileordirectory) and exists(filepath): |
|
271 return compile_template_file(filepath) |
|
272 raise Exception('no such template %s' % self.filename) |
|
273 _compiled_template = cached(_compiled_template, 0) |
|