cubicweb/ext/tal.py
changeset 11057 0b59724cb3f2
parent 10614 57dfde80df11
child 11767 432f87a63057
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     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)