Source code for simpletal.simpleTALUtils

# -*- coding: iso-8859-1 -*-

#    Copyright (c) 2016, Jan Brohl <janbrohl@t-online.de>
#    All rights reserved.
#    See LICENSE.txt

#    Copyright (c) 2005 Colin Stewart (http://www.owlfish.com/)
#    All rights reserved.
#
#    Redistribution and use in source and binary forms, with or without
#    modification, are permitted provided that the following conditions
#    are met:
#    1. Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#    2. Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#    3. The name of the author may not be used to endorse or promote products
#       derived from this software without specific prior written permission.
#
#    THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
#    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
#    OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
#    IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
#    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
#    NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
#    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
#    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
#    THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#    If you make any bug fixes or feature enhancements please let me know!

""" simpleTALUtils
		
		This module is holds utilities that make using SimpleTAL easier. 
		Initially this is just the HTMLStructureCleaner class, used to clean
		up HTML that can then be used as 'structure' content.
		
"""

from __future__ import absolute_import

import os.path
import os
import stat
import threading
import sys
import codecs
import cgi
import re
import xml.sax
import xml.sax.handler
import io
import simpletal.simpleTAL
import simpletal.simpleTALES
import simpletal.simpleTALConstants

# used to check if a path points to an HTML-file
HTML_EXT_REGEX = re.compile(".*[.]html?$", re.IGNORECASE)

# used to check quite strictly that a name does not have a special meaning
# for the filesystem
SAFE_NAME_REGEX = re.compile("(?:[a-zA-Z0-9]+[_-]+)*[a-zA-Z0-9]+$")


[docs]class TemplateCache(object): """ A TemplateCache is a multi-thread safe object that caches compiled templates. This cache only works with file based templates, the ctime of the file is checked on each hit, if the file has changed the template is re-compiled. """ def __init__(self): self.templateCache = {} self.cacheLock = threading.Lock() # debugging info self.hits = 0 self.misses = 0
[docs] def isHTML(self, name): return HTML_EXT_REGEX.match(name) is not None
[docs] def getTemplate(self, name, inputEncoding='UTF-8-SIG'): """ Name should be the path of a template file. If self.isHTML(name) it is treated as an HTML Template, otherwise it's treated as an XML Template. If the template file has changed since the last cache it will be re-compiled. inputEncoding is only used for HTML templates, and should be the encoding that the template is stored in. """ return self._cacheTemplate_(name, inputEncoding)
[docs] def getXMLTemplate(self, name): """ Name should be the path of an XML template file. """ return self._cacheTemplate_(name, None, xmlTemplate=1)
def _cacheTemplate_(self, name, inputEncoding, xmlTemplate=0): with self.cacheLock: template, oldmtime = self.templateCache.get(name, (None, None)) mtime = os.path.getmtime(name) if template is not None: if (oldmtime == mtime): # Cache hit! self.hits += 1 return template # Cache miss, let's cache this template with open(name, 'r') as tempFile: if (xmlTemplate): # We know it is XML template = simpletal.simpleTAL.compileXMLTemplate(tempFile) else: # We have to guess... if self.isHTML(name): template = simpletal.simpleTAL.compileHTMLTemplate( tempFile, inputEncoding) else: template = simpletal.simpleTAL.compileXMLTemplate( tempFile) self.templateCache[name] = ( template, mtime) self.misses += 1 return template
[docs]class TemplateFolder(object): # TODO: write tests, docs, find better name def __init__(self, root, getfunc, ext=".html", path=tuple()): self._ext = ext self._root = root self._getfunc = getfunc self._path = path def __getattr__(self, name): if SAFE_NAME_REGEX.match(name) is None: raise AttributeError("%r is not allowed" % name) if name == "container" and self._path: return self.__class__(self._root, self._getfunc, self._ext, self._path[:-1]) newPath = self._path + (name,) fullPath = os.path.join(self._root, *newPath) if os.path.isdir(fullPath): return self.__class__(self._root, self._getfunc, self._ext, newPath) elif os.path.isfile(fullPath + self._ext): return self._getfunc(fullPath + self._ext) else: raise AttributeError(name) def __iter__(self): e = self._ext le = len(e) for name in sorted(os.listdir(os.path.join(self._root, *self._path))): if name.endswith(e): n = name[:-le] if SAFE_NAME_REGEX.match(n): yield n elif SAFE_NAME_REGEX.match(name): yield name
[docs]class TemplateWrapper(object): # TODO: write tests, docs, find better name def __init__(self, template, contextGlobals={}, allowPythonPath=False): self.template = template self.contextGlobals = contextGlobals self.allowPythonPath = allowPythonPath def __call__(self, func): return (lambda *args, **kwargs: self.expand(func(*args, **kwargs)))
[docs] def expand(self, options=tuple(), updateGlobals={}, **kwGlobals): g = self.contextGlobals.copy() g.update(updateGlobals) g.update(kwGlobals) ctx = simpletal.simpleTALES.Context(options, self.allowPythonPath) for k, v in g.items(): ctx.addGlobal(k, v) f = io.StringIO() self.template.expand(ctx, f) return f.getvalue()
# TODO: write tests, docs, find better name
[docs]def wrapperLoader(templateDir="templates", standardGlobals={}): cache = TemplateCache() root = TemplateFolder(templateDir, cache.getTemplate) standardGlobals = standardGlobals.copy() standardGlobals["root"] = root def load(path): pathtuple = tuple(path.strip("/").split("/")) container = TemplateFolder( templateDir, cache.getTemplate, path=pathtuple[:-1]) contextGlobals = standardGlobals.copy() contextGlobals["container"] = container loader = TemplateFolder(templateDir, (lambda name: TemplateWrapper( cache.getTemplate(name), contextGlobals)), path=pathtuple[:-1]) return getattr(loader, pathtuple[-1]) return load
[docs]class MacroExpansionInterpreter (simpletal.simpleTAL.TemplateInterpreter): def __init__(self): simpletal.simpleTAL.TemplateInterpreter.__init__(self) # Override the standard interpreter way of doing things. self.macroStateStack = [] self.commandHandler.update({simpletal.simpleTALConstants.TAL_DEFINE: self.cmdNoOp, simpletal.simpleTALConstants.TAL_CONDITION: self.cmdNoOp, simpletal.simpleTALConstants.TAL_REPEAT: self.cmdNoOp, simpletal.simpleTALConstants.TAL_CONTENT: self.cmdNoOp, simpletal.simpleTALConstants.TAL_ATTRIBUTES: self.cmdNoOp, simpletal.simpleTALConstants.TAL_OMITTAG: self.cmdNoOp, simpletal.simpleTALConstants.TAL_START_SCOPE: self.cmdStartScope, simpletal.simpleTALConstants.TAL_OUTPUT: self.cmdOutput, simpletal.simpleTALConstants.TAL_STARTTAG: self.cmdOutputStartTag, simpletal.simpleTALConstants.TAL_ENDTAG_ENDSCOPE: self.cmdEndTagEndScope, simpletal.simpleTALConstants.METAL_USE_MACRO: self.cmdUseMacro, simpletal.simpleTALConstants.METAL_DEFINE_SLOT: self.cmdDefineSlot, simpletal.simpleTALConstants.TAL_NOOP: self.cmdNoOp}) self.inMacro = None self.macroArg = None # Original cmdOutput # Original cmdEndTagEndScope
[docs] def popProgram(self): self.inMacro = self.macroStateStack.pop() simpletal.simpleTAL.TemplateInterpreter.popProgram(self)
[docs] def pushProgram(self): self.macroStateStack.append(self.inMacro) simpletal.simpleTAL.TemplateInterpreter.pushProgram(self)
[docs] def cmdOutputStartTag(self, command, args): newAtts = [] for att, value in self.originalAttributes.items(): if (self.macroArg is not None and att == "metal:define-macro"): newAtts.append(("metal:use-macro", self.macroArg)) elif (self.inMacro and att == "metal:define-slot"): newAtts.append(("metal:fill-slot", value)) else: newAtts.append((att, value)) self.macroArg = None self.currentAttributes = newAtts simpletal.simpleTAL.TemplateInterpreter.cmdOutputStartTag( self, command, args)
[docs] def cmdUseMacro(self, command, args): simpletal.simpleTAL.TemplateInterpreter.cmdUseMacro( self, command, args) if (self.tagContent is not None): # We have a macro, add the args to the in-macro list self.inMacro = 1 self.macroArg = args[0]
[docs] def cmdEndTagEndScope(self, command, args): # Args: tagName, omitFlag if (self.tagContent is not None): contentType, resultVal = self.tagContent if (contentType): if (isinstance(resultVal, simpletal.simpleTAL.Template)): # We have another template in the context, evaluate it! # Save our state! self.pushProgram() resultVal.expandInline(self.context, self.file, self) # Restore state self.popProgram() # End of the macro expansion (if any) so clear the # parameters self.slotParameters = {} # End of the macro self.inMacro = 0 else: if (isinstance(resultVal, unicode)): self.file.write(resultVal) elif (isinstance(resultVal, str)): self.file.write(unicode(resultVal, 'ascii')) else: self.file.write(unicode(str(resultVal), 'ascii')) else: if (isinstance(resultVal, unicode)): self.file.write(cgi.escape(resultVal)) elif (isinstance(resultVal, str)): self.file.write(cgi.escape(unicode(resultVal, 'ascii'))) else: self.file.write(cgi.escape( unicode(str(resultVal), 'ascii'))) if (self.outputTag and not args[1]): self.file.write('</' + args[0] + '>') if (self.movePCBack is not None): self.programCounter = self.movePCBack return if (self.localVarsDefined): self.context.popLocals() (self.movePCForward, self.movePCBack, self.outputTag, self.originalAttributes, self.currentAttributes, self.repeatVariable, self.tagContent, self.localVarsDefined) = self.scopeStack.pop() self.programCounter += 1
[docs]def ExpandMacros(context, template, outputEncoding="utf-8"): out = io.StringIO() interp = MacroExpansionInterpreter() interp.initialise(context, out) template.expand(context, out, outputEncoding=outputEncoding, interpreter=interp) return out.getvalue().encode(outputEncoding)