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