common/tal.py
changeset 0 b97547f5f1fa
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     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