ext/tal.py
author Pierre-Yves David <pierre-yves.david@logilab.fr>
Mon, 25 Mar 2013 15:28:18 +0100
changeset 8787 1b3b7284377f
parent 8695 358d8bed9626
child 10590 7629902e7554
permissions -rw-r--r--
[session] allow writable tx_attr and use it for commit_state This keep clarifying the code.

# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
"""provides simpleTAL extensions for CubicWeb

"""

__docformat__ = "restructuredtext en"

import sys
import re
from os.path import exists, isdir, join
from logging import getLogger
from StringIO import StringIO

from simpletal import simpleTAL, simpleTALES

from logilab.common.decorators import cached

LOGGER = getLogger('cubicweb.tal')


class LoggerAdapter(object):
    def __init__(self, tal_logger):
        self.tal_logger = tal_logger

    def debug(self, msg):
        LOGGER.debug(msg)

    def warn(self, msg):
        LOGGER.warning(msg)

    def __getattr__(self, attrname):
        return getattr(self.tal_logger, attrname)


class CubicWebContext(simpleTALES.Context):
    """add facilities to access entity / resultset"""

    def __init__(self, options=None, allowPythonPath=1):
        simpleTALES.Context.__init__(self, options, allowPythonPath)
        self.log = LoggerAdapter(self.log)

    def update(self, context):
        for varname, value in context.items():
            self.addGlobal(varname, value)

    def addRepeat(self, name, var, initialValue):
        simpleTALES.Context.addRepeat(self, name, var, initialValue)

# XXX FIXME need to find a clean to define OPCODE values for extensions
I18N_CONTENT = 18
I18N_REPLACE = 19
RQL_EXECUTE  = 20
# simpleTAL uses the OPCODE values to define priority over commands.
# TAL_ITER should have the same priority than TAL_REPEAT (i.e. 3), but
# we can't use the same OPCODE for two different commands without changing
# the simpleTAL implementation. Another solution would be to totally override
# the REPEAT implementation with the ITER one, but some specific operations
# (involving len() for instance) are not implemented for ITER, so we prefer
# to keep both implementations for now, and to fool simpleTAL by using a float
# number between 3 and 4
TAL_ITER     = 3.1


# FIX simpleTAL HTML 4.01 stupidity
# (simpleTAL never closes tags like INPUT, IMG, HR ...)
simpleTAL.HTML_FORBIDDEN_ENDTAG.clear()

class CubicWebTemplateCompiler(simpleTAL.HTMLTemplateCompiler):
    """extends default compiler by adding i18n:content commands"""

    def __init__(self):
        simpleTAL.HTMLTemplateCompiler.__init__(self)
        self.commandHandler[I18N_CONTENT] = self.compile_cmd_i18n_content
        self.commandHandler[I18N_REPLACE] = self.compile_cmd_i18n_replace
        self.commandHandler[RQL_EXECUTE] = self.compile_cmd_rql
        self.commandHandler[TAL_ITER] = self.compile_cmd_tal_iter

    def setTALPrefix(self, prefix):
        simpleTAL.TemplateCompiler.setTALPrefix(self, prefix)
        self.tal_attribute_map['i18n:content'] = I18N_CONTENT
        self.tal_attribute_map['i18n:replace'] = I18N_REPLACE
        self.tal_attribute_map['rql:execute'] = RQL_EXECUTE
        self.tal_attribute_map['tal:iter'] = TAL_ITER

    def compile_cmd_i18n_content(self, argument):
        # XXX tal:content structure=, text= should we support this ?
        structure_flag = 0
        return (I18N_CONTENT, (argument, False, structure_flag, self.endTagSymbol))

    def compile_cmd_i18n_replace(self, argument):
        # XXX tal:content structure=, text= should we support this ?
        structure_flag = 0
        return (I18N_CONTENT, (argument, True, structure_flag, self.endTagSymbol))

    def compile_cmd_rql(self, argument):
        return (RQL_EXECUTE, (argument, self.endTagSymbol))

    def compile_cmd_tal_iter(self, argument):
        original_id, (var_name, expression, end_tag_symbol) = \
                     simpleTAL.HTMLTemplateCompiler.compileCmdRepeat(self, argument)
        return (TAL_ITER, (var_name, expression, self.endTagSymbol))

    def getTemplate(self):
        return CubicWebTemplate(self.commandList, self.macroMap, self.symbolLocationTable)

    def compileCmdAttributes (self, argument):
        """XXX modified to support single attribute
        definition ending by a ';'

        backport this to simpleTAL
        """
        # Compile tal:attributes into attribute command
        # Argument: [(attributeName, expression)]

        # Break up the list of attribute settings first
        commandArgs = []
        # We only want to match semi-colons that are not escaped
        argumentSplitter =  re.compile(r'(?<!;);(?!;)')
        for attributeStmt in argumentSplitter.split(argument):
            if not attributeStmt.strip():
                continue
            #  remove any leading space and un-escape any semi-colons
            attributeStmt = attributeStmt.lstrip().replace(';;', ';')
            # Break each attributeStmt into name and expression
            stmtBits = attributeStmt.split(' ')
            if (len (stmtBits) < 2):
                # Error, badly formed attributes command
                msg = "Badly formed attributes command '%s'.  Attributes commands must be of the form: 'name expression[;name expression]'" % argument
                self.log.error(msg)
                raise simpleTAL.TemplateParseException(self.tagAsText(self.currentStartTag), msg)
            attName = stmtBits[0]
            attExpr = " ".join(stmtBits[1:])
            commandArgs.append((attName, attExpr))
        return (simpleTAL.TAL_ATTRIBUTES, commandArgs)


class CubicWebTemplateInterpreter(simpleTAL.TemplateInterpreter):
    """provides implementation for interpreting cubicweb extensions"""
    def __init__(self):
        simpleTAL.TemplateInterpreter.__init__(self)
        self.commandHandler[I18N_CONTENT] = self.cmd_i18n
        self.commandHandler[TAL_ITER] = self.cmdRepeat
        # self.commandHandler[RQL_EXECUTE] = self.cmd_rql

    def cmd_i18n(self, command, args):
        """i18n:content and i18n:replace implementation"""
        string, replace_flag, structure_flag, end_symbol = args
        if replace_flag:
            self.outputTag = 0
        result = self.context.globals['_'](string)
        self.tagContent = (0, result)
        self.movePCForward = self.symbolTable[end_symbol]
        self.programCounter += 1


class CubicWebTemplate(simpleTAL.HTMLTemplate):
    """overrides HTMLTemplate.expand() to systematically use CubicWebInterpreter
    """
    def expand(self, context, outputFile):
        interpreter = CubicWebTemplateInterpreter()
        interpreter.initialise(context, outputFile)
        simpleTAL.HTMLTemplate.expand(self, context, outputFile,# outputEncoding='unicode',
                                      interpreter=interpreter)

    def expandInline(self, context, outputFile, interpreter):
        """ Internally used when expanding a template that is part of a context."""
        try:
            interpreter.execute(self)
        except UnicodeError as unierror:
            LOGGER.exception(str(unierror))
            raise simpleTALES.ContextContentException("found non-unicode %r string in Context!" % unierror.args[1]), None, sys.exc_info()[-1]


def compile_template(template):
    """compiles a TAL template string
    :type template: unicode
    :param template: a TAL-compliant template string
    """
    string_buffer = StringIO(template)
    compiler = CubicWebTemplateCompiler()
    compiler.parseTemplate(string_buffer) # , inputEncoding='unicode')
    return compiler.getTemplate()


def compile_template_file(filepath):
    """compiles a TAL template file
    :type filepath: str
    :param template: path of the file to compile
    """
    fp = file(filepath)
    file_content = unicode(fp.read()) # template file should be pure ASCII
    fp.close()
    return compile_template(file_content)


def evaluatePython (self, expr):
    if not self.allowPythonPath:
        return self.false
    globals = {}
    for name, value in self.globals.items():
        if isinstance (value, simpleTALES.ContextVariable):
            value = value.rawValue()
        globals[name] = value
    globals['path'] = self.pythonPathFuncs.path
    globals['string'] = self.pythonPathFuncs.string
    globals['exists'] = self.pythonPathFuncs.exists
    globals['nocall'] = self.pythonPathFuncs.nocall
    globals['test'] = self.pythonPathFuncs.test
    locals = {}
    for name, value in self.locals.items():
        if (isinstance (value, simpleTALES.ContextVariable)):
            value = value.rawValue()
        locals[name] = value
    # XXX precompile expr will avoid late syntax error
    try:
        result = eval(expr, globals, locals)
    except Exception as ex:
        ex = ex.__class__('in %r: %s' % (expr, ex))
        raise ex, None, sys.exc_info()[-1]
    if (isinstance (result, simpleTALES.ContextVariable)):
        return result.value()
    return result

simpleTALES.Context.evaluatePython = evaluatePython


class talbased(object):
    def __init__(self, filename, write=True):
##         if not osp.isfile(filepath):
##             # print "[tal.py] just for tests..."
##             # get parent frame
##             directory = osp.abspath(osp.dirname(sys._getframe(1).f_globals['__file__']))
##             filepath = osp.join(directory, filepath)
        self.filename = filename
        self.write = write

    def __call__(self, viewfunc):
        def wrapped(instance, *args, **kwargs):
            variables = viewfunc(instance, *args, **kwargs)
            html = instance.tal_render(self._compiled_template(instance), variables)
            if self.write:
                instance.w(html)
            else:
                return html
        return wrapped

    def _compiled_template(self, instance):
        for fileordirectory in instance.config.appobjects_path():
            filepath = join(fileordirectory, self.filename)
            if isdir(fileordirectory) and exists(filepath):
                return compile_template_file(filepath)
        raise Exception('no such template %s' % self.filename)
    _compiled_template = cached(_compiled_template, 0)