# -*- 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!
""" simpleTAL Interpreter
The classes in this module implement the TAL language, expanding
both XML and HTML templates.
"""
from __future__ import unicode_literals
from __future__ import absolute_import
import xml.sax
import cgi
import codecs
import re
import xml.sax.saxutils
import copy
import sys
import simpletal.simpleTALES
from simpletal.simpleTALConstants import *
import logging
import io
if sys.version_info >= (3, 0):
unicode = str
unichr = chr
import html.parser as HTMLParser
import html.entities as htmlentitydefs
else:
import HTMLParser
import htmlentitydefs
name2unicode = dict((k, unichr(v))
for k, v in htmlentitydefs.name2codepoint.items())
METAL_NAME_REGEX = re.compile("[a-zA-Z_][a-zA-Z0-9_]*")
SINGLETON_XML_REGEX = re.compile(b'^<[^\s/>]+(?:\s*[^=>]+="[^">]+")*\s*/>')
try:
# Is PyXML's LexicalHandler available?
from xml.sax.saxlib import LexicalHandler
use_lexical_handler = 1
except ImportError:
use_lexical_handler = 0
[docs] class LexicalHandler(object):
"Dummy lexical handdler class"
pass
try:
# Is PyXML's DOM2SAX available?
import xml.dom.ext.Dom2Sax
use_dom2sax = 1
except ImportError:
use_dom2sax = 0
[docs]class TemplateInterpreter(object):
def __init__(self):
self.programStack = []
self.commandList = None
self.symbolTable = None
self.slotParameters = {}
self.commandHandler = {TAL_DEFINE: self.cmdDefine,
TAL_CONDITION: self.cmdCondition,
TAL_REPEAT: self.cmdRepeat,
TAL_CONTENT: self.cmdContent,
TAL_ATTRIBUTES: self.cmdAttributes,
TAL_OMITTAG: self.cmdOmitTag,
TAL_START_SCOPE: self.cmdStartScope,
TAL_OUTPUT: self.cmdOutput,
TAL_STARTTAG: self.cmdOutputStartTag,
TAL_ENDTAG_ENDSCOPE:
self.cmdEndTagEndScope,
METAL_USE_MACRO: self.cmdUseMacro,
METAL_DEFINE_SLOT: self.cmdDefineSlot,
TAL_NOOP: self.cmdNoOp}
[docs] def tagAsText(self, tag_atts, singletonFlag=0):
""" This returns a tag as text.
"""
(tag, atts) = tag_atts
result = ["<"]
result.append(tag)
for attName, attValue in atts:
result.append(' ')
result.append(attName)
result.append('=')
result.append(xml.sax.saxutils.quoteattr(attValue))
if (singletonFlag):
result.append(" />")
else:
result.append(">")
return "".join(result)
[docs] def initialise(self, context, outputFile):
self.context = context
self.file = outputFile
[docs] def cleanState(self):
self.scopeStack = []
self.programCounter = 0
self.movePCForward = None
self.movePCBack = None
self.outputTag = 1
self.originalAttributes = {}
self.currentAttributes = []
# Used in repeat only.
self.repeatAttributesCopy = []
self.currentSlots = {}
self.repeatVariable = None
self.tagContent = None
# tagState flag as to whether there are any local variables to pop
self.localVarsDefined = 0
# Pass in the parameters
self.currentSlots = self.slotParameters
[docs] def popProgram(self):
v, self.commandList, self.symbolTable = self.programStack.pop()
(self.programCounter, self.scopeStack, self.slotParameters,
self.currentSlots, self.movePCForward, self.movePCBack,
self.outputTag, self.originalAttributes,
self.currentAttributes, self.repeatVariable,
self.repeatAttributesCopy, self.tagContent,
self.localVarsDefined) = v
[docs] def pushProgram(self):
v = (self.programCounter, self.scopeStack,
self.slotParameters, self.currentSlots,
self.movePCForward, self.movePCBack, self.outputTag,
self.originalAttributes, self.currentAttributes,
self.repeatVariable, self.repeatAttributesCopy,
self.tagContent, self.localVarsDefined)
self.programStack.append((v, self.commandList, self.symbolTable))
[docs] def execute(self, template):
self.cleanState()
(self.commandList, self.programCounter, programLength,
self.symbolTable) = template.getProgram()
cmndList = self.commandList
while (self.programCounter < programLength):
cmnd = cmndList[self.programCounter]
# print "PC: %s - Executing command: %s" % (unicode
# (self.programCounter), unicode (cmnd))
self.commandHandler[cmnd[0]](cmnd[0], cmnd[1])
[docs] def cmdDefine(self, command, args):
""" args: [(isLocalFlag (Y/n), variableName, variablePath),...]
Define variables in either the local or global context
"""
foundLocals = 0
for isLocal, varName, varPath in args:
result = self.context.evaluate(varPath, self.originalAttributes)
if (isLocal):
if (not foundLocals):
foundLocals = 1
self.context.pushLocals()
self.context.setLocal(varName, result)
else:
self.context.addGlobal(varName, result)
self.localVarsDefined = foundLocals
self.programCounter += 1
[docs] def cmdCondition(self, command, args):
""" args: expression, endTagSymbol
Conditionally continues with execution of all content contained
by it.
"""
result = self.context.evaluate(args[0], self.originalAttributes)
#~ if (result is None or (not result)):
conditionFalse = 0
if (result is None):
conditionFalse = 1
else:
if (not result):
conditionFalse = 1
try:
temp = len(result)
if (temp == 0):
conditionFalse = 1
except:
# Result is not a sequence.
pass
if (conditionFalse):
# Nothing to output - evaluated to false.
self.outputTag = 0
self.tagContent = None
self.programCounter = self.symbolTable[args[1]]
return
self.programCounter += 1
[docs] def cmdRepeat(self, command, args):
""" args: (varName, expression, endTagSymbol)
Repeats anything in the cmndList
"""
if (self.repeatVariable is not None):
# We are already part way through a repeat
# Restore any attributes that might have been changed.
if (self.currentAttributes != self.repeatAttributesCopy):
self.currentAttributes = copy.copy(self.repeatAttributesCopy)
self.outputTag = 1
self.tagContent = None
self.movePCForward = None
try:
self.repeatVariable.increment()
self.context.setLocal(
args[0], self.repeatVariable.getCurrentValue())
self.programCounter += 1
return
except IndexError as e:
# We have finished the repeat
self.repeatVariable = None
self.context.removeRepeat(args[0])
# The locals were pushed in context.addRepeat
self.context.popLocals()
self.movePCBack = None
# Suppress the final close tag and content
self.tagContent = None
self.outputTag = 0
self.programCounter = self.symbolTable[args[2]]
# Restore the state of repeatAttributesCopy in case we are
# nested.
self.repeatAttributesCopy = self.scopeStack.pop()
return
# The first time through this command
result = self.context.evaluate(args[1], self.originalAttributes)
if (result is not None and result is simpletal.simpleTALES.DEFAULTVALUE):
# Leave everything un-touched.
self.programCounter += 1
return
try:
# We have three options, either the result is a natural sequence,
# an iterator., or something that can produce an iterator.
isSequence = len(result)
if (isSequence):
# Only setup if we have a sequence with length
self.repeatVariable = simpletal.simpleTALES.RepeatVariable(
result)
else:
# Delete the tags and their contents
self.outputTag = 0
self.programCounter = self.symbolTable[args[2]]
return
except:
# Not a natural sequence, can it produce an iterator?
try:
it = iter(result)
except TypeError:
# Just a plain object, let's not loop
# Delete the tags and their contents
self.outputTag = 0
self.programCounter = self.symbolTable[args[2]]
return
else:
# We can get an iterator!
self.repeatVariable = simpletal.simpleTALES.IteratorRepeatVariable(
it)
try:
curValue = self.repeatVariable.getCurrentValue()
except IndexError as e:
# The iterator ran out of values before we started - treat as an
# empty list
self.outputTag = 0
self.repeatVariable = None
self.programCounter = self.symbolTable[args[2]]
return
# We really do want to repeat - so lets do it
self.movePCBack = self.programCounter
self.context.addRepeat(args[0], self.repeatVariable, curValue)
# We keep the old state of the repeatAttributesCopy for nested loops
self.scopeStack.append(self.repeatAttributesCopy)
# Keep a copy of the current attributes for this tag
self.repeatAttributesCopy = copy.copy(self.currentAttributes)
self.programCounter += 1
[docs] def cmdContent(self, command, args):
""" args: (replaceFlag, structureFlag, expression, endTagSymbol)
Expands content
"""
result = self.context.evaluate(args[2], self.originalAttributes)
if (result is None):
if (args[0]):
# Only output tags if this is a content not a replace
self.outputTag = 0
# Output none of our content or the existing content, but
# potentially the tags
self.movePCForward = self.symbolTable[args[3]]
self.programCounter += 1
return
elif (result is not simpletal.simpleTALES.DEFAULTVALUE):
# We have content, so let's suppress the natural content and output
# this!
if (args[0]):
self.outputTag = 0
self.tagContent = (args[1], result)
self.movePCForward = self.symbolTable[args[3]]
self.programCounter += 1
return
else:
# Default, let's just run through as normal
self.programCounter += 1
return
[docs] def cmdAttributes(self, command, args):
""" args: [(attributeName, expression)]
Add, leave, or remove attributes from the start tag
"""
attsToRemove = set()
newAtts = []
for attName, attExpr in args:
resultVal = self.context.evaluate(attExpr, self.originalAttributes)
if (resultVal is None):
# Remove this attribute from the current attributes
attsToRemove.add(attName)
elif (resultVal is not simpletal.simpleTALES.DEFAULTVALUE):
# We have a value - let's use it!
attsToRemove.add(attName)
if (isinstance(resultVal, unicode)):
escapedAttVal = resultVal
elif (isinstance(resultVal, bytes)):
# THIS IS NOT A BUG!
# Use Unicode in the Context object if you are not using
# Ascii
escapedAttVal = unicode(resultVal, 'ascii')
else:
# THIS IS NOT A BUG!
# Use Unicode in the Context object if you are not using
# Ascii
escapedAttVal = unicode(resultVal)
newAtts.append((attName, escapedAttVal))
# Copy over the old attributes
for oldAttName, oldAttValue in self.currentAttributes:
if (oldAttName not in attsToRemove):
newAtts.append((oldAttName, oldAttValue))
self.currentAttributes = newAtts
# Evaluate all other commands
self.programCounter += 1
[docs] def cmdOmitTag(self, command, args):
""" args: expression
Conditionally turn off tag output
"""
result = self.context.evaluate(args, self.originalAttributes)
if (result is not None and result):
# Turn tag output off
self.outputTag = 0
self.programCounter += 1
[docs] def cmdOutputStartTag(self, command, args):
# Args: tagName
tagName, singletonTag = args
if (self.outputTag):
if (self.tagContent is None and singletonTag):
self.file.write(self.tagAsText(
(tagName, self.currentAttributes), 1))
else:
self.file.write(self.tagAsText(
(tagName, self.currentAttributes)))
if (self.movePCForward is not None):
self.programCounter = self.movePCForward
return
self.programCounter += 1
return
[docs] def cmdEndTagEndScope(self, command, args):
# Args: tagName, omitFlag, singletonTag
if (self.tagContent is not None):
contentType, resultVal = self.tagContent
if (contentType):
if (isinstance(resultVal, 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 = {}
else:
if (isinstance(resultVal, unicode)):
self.file.write(resultVal)
elif (isinstance(resultVal, bytes)):
# THIS IS NOT A BUG!
# Use Unicode in the Context object if you are not
# using Ascii
self.file.write(unicode(resultVal, 'ascii'))
else:
# THIS IS NOT A BUG!
# Use Unicode in the Context object if you are not
# using Ascii
self.file.write(unicode(resultVal))
else:
if (isinstance(resultVal, unicode)):
self.file.write(xml.sax.saxutils.escape(resultVal))
elif (isinstance(resultVal, bytes)):
# THIS IS NOT A BUG!
# Use Unicode in the Context object if you are not using
# Ascii
self.file.write(xml.sax.saxutils.escape(
unicode(resultVal, 'ascii')))
else:
# THIS IS NOT A BUG!
# Use Unicode in the Context object if you are not using
# Ascii
self.file.write(
xml.sax.saxutils.escape(unicode(resultVal)))
if (self.outputTag and not args[1]):
# Do NOT output end tag if a singleton with no content
if not (args[2] and self.tagContent is None):
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 cmdOutput(self, command, args):
self.file.write(args)
self.programCounter += 1
[docs] def cmdStartScope(self, command, args):
""" args: (originalAttributes, currentAttributes)
Pushes the current state onto the stack, and sets up the new state
"""
self.scopeStack.append((self.movePCForward, self.movePCBack,
self.outputTag,
self.originalAttributes,
self.currentAttributes,
self.repeatVariable, self.tagContent,
self.localVarsDefined))
self.movePCForward = None
self.movePCBack = None
self.outputTag = 1
self.originalAttributes = args[0]
self.currentAttributes = args[1]
self.repeatVariable = None
self.tagContent = None
self.localVarsDefined = 0
self.programCounter += 1
[docs] def cmdNoOp(self, command, args):
self.programCounter += 1
[docs] def cmdUseMacro(self, command, args):
""" args: (macroExpression, slotParams, endTagSymbol)
Evaluates the expression, if it resolves to a SubTemplate it then places
the slotParams into currentSlots and then jumps to the end tag
"""
value = self.context.evaluate(args[0], self.originalAttributes)
if (value is None):
# Don't output anything
self.outputTag = 0
# Output none of our content or the existing content
self.movePCForward = self.symbolTable[args[2]]
self.programCounter += 1
return
if (value is not simpletal.simpleTALES.DEFAULTVALUE and isinstance(value, SubTemplate)):
# We have a macro, so let's use it
self.outputTag = 0
self.slotParameters = args[1]
self.tagContent = (1, value)
# NOTE: WE JUMP STRAIGHT TO THE END TAG, NO OTHER TAL/METAL
# COMMANDS ARE EVALUATED.
self.programCounter = self.symbolTable[args[2]]
return
else:
# Default, let's just run through as normal
self.programCounter += 1
return
[docs] def cmdDefineSlot(self, command, args):
""" args: (slotName, endTagSymbol)
If the slotName is filled then that is used, otherwise the original conent
is used.
"""
if (args[0] in self.currentSlots):
# This slot is filled, so replace us with that content
self.outputTag = 0
self.tagContent = (1, self.currentSlots[args[0]])
# Output none of our content or the existing content
# NOTE: NO FURTHER TAL/METAL COMMANDS ARE EVALUATED
self.programCounter = self.symbolTable[args[1]]
return
# Slot isn't filled, so just use our own content
self.programCounter += 1
return
[docs]class HTMLTemplateInterpreter (TemplateInterpreter):
def __init__(self, minimizeBooleanAtts=0):
TemplateInterpreter.__init__(self)
self.minimizeBooleanAtts = minimizeBooleanAtts
if (minimizeBooleanAtts):
# Override the tagAsText method for this instance
self.tagAsText = self.tagAsTextMinimizeAtts
[docs] def tagAsTextMinimizeAtts(self, tag_atts, singletonFlag=0):
""" This returns a tag as text.
"""
(tag, atts) = tag_atts
result = ["<"]
result.append(tag)
upperTag = tag.upper()
for attName, attValue in atts:
if ((upperTag, attName.upper()) in HTML_BOOLEAN_ATTS):
# We should output a minimised boolean value
result.append(' ')
result.append(attName)
else:
result.append(' ')
result.append(attName)
result.append('=')
result.append(xml.sax.saxutils.quoteattr(attValue))
if (singletonFlag):
result.append(" />")
else:
result.append(">")
return "".join(result)
[docs]class Template(object):
def __init__(self, commands, macros, symbols, doctype=None):
self.commandList = commands
self.macros = macros
self.symbolTable = symbols
self.doctype = doctype
# Setup the macros
for macro in self.macros.values():
macro.setParentTemplate(self)
# Setup the slots
for cmnd, arg in self.commandList:
if (cmnd == METAL_USE_MACRO):
# Set the parent of each slot
slotMap = arg[1]
for slot in slotMap.values():
slot.setParentTemplate(self)
[docs] def expand(self, context, outputFile, outputEncoding=None, interpreter=None):
""" This method will write to the outputFile, using the encoding specified,
the expanded version of this template. The context passed in is used to resolve
all expressions with the template.
"""
# This method must wrap outputFile if required by the encoding, and write out
# any template pre-amble (DTD, Encoding, etc)
self.expandInline(context, outputFile, interpreter)
[docs] def expandInline(self, context, outputFile, interpreter=None):
""" Internally used when expanding a template that is part of a context."""
if (interpreter is None):
ourInterpreter = TemplateInterpreter()
ourInterpreter.initialise(context, outputFile)
else:
ourInterpreter = interpreter
try:
ourInterpreter.execute(self)
except UnicodeError as unierror:
logging.error(
"UnicodeError caused by placing a non-Unicode string in the Context object.")
raise simpletal.simpleTALES.ContextContentException(
"Found non-unicode string in Context!")
[docs] def getProgram(self):
""" Returns a tuple of (commandList, startPoint, endPoint, symbolTable) """
return (self.commandList, 0, len(self.commandList), self.symbolTable)
def __str__(self):
result = "Commands:\n"
index = 0
for cmd in self.commandList:
if (cmd[0] != METAL_USE_MACRO):
result = result + "\n[%s] %s" % (unicode(index), unicode(cmd))
else:
result = result + \
"\n[%s] %s, (%s{" % (unicode(index),
unicode(cmd[0]), unicode(cmd[1][0]))
for slot in cmd[1][1].keys():
result = result + \
"%s: %s" % (slot, unicode(cmd[1][1][slot]))
result = result + "}, %s)" % unicode(cmd[1][2])
index += 1
result = result + "\n\nSymbols:\n"
for symbol in self.symbolTable.keys():
result = (result + "Symbol: " + unicode(symbol)
+ " points to: " + unicode(self.symbolTable[symbol])
+ ", which is command: "
+ unicode(self.commandList[self.symbolTable[symbol]])
+ "\n")
result = result + "\n\nMacros:\n"
for macro in self.macros.keys():
result = result + "Macro: " + \
unicode(macro) + " value of: " + unicode(self.macros[macro])
return result
[docs]class SubTemplate (Template):
""" A SubTemplate is part of another template, and is used for the METAL implementation.
The two uses for this class are:
1 - metal:define-macro results in a SubTemplate that is the macro
2 - metal:fill-slot results in a SubTemplate that is a parameter to metal:use-macro
"""
def __init__(self, startRange, endRangeSymbol):
""" The parentTemplate is the template for which we are a sub-template.
The startRange and endRange are indexes into the parent templates command list,
and defines the range of commands that we can execute
"""
Template.__init__(self, [], {}, {})
self.startRange = startRange
self.endRangeSymbol = endRangeSymbol
[docs] def setParentTemplate(self, parentTemplate):
self.parentTemplate = parentTemplate
self.commandList = parentTemplate.commandList
self.symbolTable = parentTemplate.symbolTable
[docs] def getProgram(self):
""" Returns a tuple of (commandList, startPoint, endPoint, symbolTable) """
return (self.commandList, self.startRange,
self.symbolTable[self.endRangeSymbol] + 1, self.symbolTable)
def __str__(self):
endRange = self.symbolTable[self.endRangeSymbol]
result = "SubTemplate from %s to %s\n" % (
unicode(self.startRange), unicode(endRange))
return result
[docs]class HTMLTemplate (Template):
"""A specialised form of a template that knows how to output HTML
"""
def __init__(self, commands, macros, symbols, doctype=None, minimizeBooleanAtts=0):
self.minimizeBooleanAtts = minimizeBooleanAtts
Template.__init__(self, commands, macros, symbols, doctype=None)
[docs] def expand(self, context, outputFile, outputEncoding="utf-8", interpreter=None):
""" This method will write to the outputFile, using the encoding specified,
the expanded version of this template. The context passed in is used to resolve
all expressions with the template.
"""
# This method must wrap outputFile if required by the encoding, and write out
# any template pre-amble (DTD, Encoding, etc)
if isinstance(outputFile, io.TextIOBase):
encodingFile = outputFile
else:
writer = codecs.getwriter(outputEncoding)
encodingFile = writer(outputFile, errors='xmlcharrefreplace')
self.expandInline(context, encodingFile, interpreter)
[docs] def expandInline(self, context, outputFile, interpreter=None):
""" Ensure we use the HTMLTemplateInterpreter"""
if (interpreter is None):
ourInterpreter = HTMLTemplateInterpreter(
minimizeBooleanAtts=self.minimizeBooleanAtts)
ourInterpreter.initialise(context, outputFile)
else:
ourInterpreter = interpreter
Template.expandInline(self, context, outputFile, ourInterpreter)
[docs]class XMLTemplate (Template):
"""A specialised form of a template that knows how to output XML
"""
def __init__(self, commands, macros, symbols, doctype=None):
Template.__init__(self, commands, macros, symbols)
self.doctype = doctype
[docs] def expand(self, context, outputFile, outputEncoding="utf-8",
docType=None, suppressXMLDeclaration=0, interpreter=None):
""" This method will write to the outputFile, using the encoding specified,
the expanded version of this template. The context passed in is used to resolve
all expressions with the template.
"""
# This method must wrap outputFile if required by the encoding, and write out
# any template pre-amble (DTD, Encoding, etc)
# Write out the XML prolog
if isinstance(outputFile, io.TextIOBase):
encodingFile = outputFile
else:
writer = codecs.getwriter(outputEncoding)
encodingFile = writer(outputFile, errors='xmlcharrefreplace')
if (not suppressXMLDeclaration):
oel = unicode(outputEncoding).lower()
if (oel != "utf-8"):
encodingFile.write(
'<?xml version="1.0" encoding="%s"?>\n' % oel)
else:
encodingFile.write('<?xml version="1.0"?>\n')
if not docType and self.doctype:
docType = self.doctype
if docType:
encodingFile.write(docType)
encodingFile.write('\n')
self.expandInline(context, encodingFile, interpreter)
[docs]class TemplateCompiler(object):
def __init__(self):
""" Initialise a template compiler.
"""
self.commandList = []
self.tagStack = []
self.symbolLocationTable = {}
self.macroMap = {}
self.endTagSymbol = 1
self.commandHandler = {TAL_DEFINE: self.compileCmdDefine,
TAL_CONDITION: self.compileCmdCondition,
TAL_REPEAT: self.compileCmdRepeat,
TAL_CONTENT: self.compileCmdContent,
TAL_REPLACE: self.compileCmdReplace,
TAL_ATTRIBUTES: self.compileCmdAttributes,
TAL_OMITTAG: self.compileCmdOmitTag,
# Metal commands
METAL_USE_MACRO: self.compileMetalUseMacro,
METAL_DEFINE_SLOT: self.compileMetalDefineSlot,
METAL_FILL_SLOT: self.compileMetalFillSlot,
METAL_DEFINE_MACRO: self.compileMetalDefineMacro}
# Default namespaces
self.setTALPrefix('tal')
self.tal_namespace_prefix_stack = []
self.metal_namespace_prefix_stack = []
self.tal_namespace_prefix_stack.append('tal')
self.setMETALPrefix('metal')
self.metal_namespace_prefix_stack.append('metal')
self.log = logging.getLogger("simpleTAL.TemplateCompiler")
[docs] def setTALPrefix(self, prefix):
self.tal_namespace_prefix = prefix
self.tal_namespace_omittag = '%s:omit-tag' % self.tal_namespace_prefix
self.tal_attribute_map = {}
self.tal_attribute_map['%s:attributes' % prefix] = TAL_ATTRIBUTES
self.tal_attribute_map['%s:content' % prefix] = TAL_CONTENT
self.tal_attribute_map['%s:define' % prefix] = TAL_DEFINE
self.tal_attribute_map['%s:replace' % prefix] = TAL_REPLACE
self.tal_attribute_map['%s:omit-tag' % prefix] = TAL_OMITTAG
self.tal_attribute_map['%s:condition' % prefix] = TAL_CONDITION
self.tal_attribute_map['%s:repeat' % prefix] = TAL_REPEAT
[docs] def popTALNamespace(self):
newPrefix = self.tal_namespace_prefix_stack.pop()
self.setTALPrefix(newPrefix)
[docs] def tagAsText(self, tag_atts, singletonFlag=0):
""" This returns a tag as text.
"""
(tag, atts) = tag_atts
result = ["<"]
result.append(tag)
for attName, attValue in atts:
result.append(' ')
result.append(attName)
result.append('=')
result.append(xml.sax.saxutils.quoteattr(attValue))
if (singletonFlag):
result.append(" />")
else:
result.append(">")
return "".join(result)
[docs] def getTemplate(self):
template = Template(self.commandList, self.macroMap,
self.symbolLocationTable)
return template
[docs] def addCommand(self, command):
if (command[0] == TAL_OUTPUT and self.commandList and self.commandList[-1][0] == TAL_OUTPUT):
# We can combine output commands
self.commandList[-1] = (TAL_OUTPUT,
self.commandList[-1][1] + command[1])
else:
self.commandList.append(command)
[docs] def addTag(self, tag, tagProperties={}):
""" Used to add a tag to the stack. Various properties can be passed in the dictionary
as being information required by the tag.
Currently supported properties are:
'command' - The (command,args) tuple associated with this command
'originalAtts' - The original attributes that include any metal/tal attributes
'endTagSymbol' - The symbol associated with the end tag for this element
'popFunctionList' - A list of functions to execute when this tag is popped
'singletonTag' - A boolean to indicate that this is a singleton flag
"""
# Add the tag to the tagStack (list of tuples (tag, properties,
# useMacroLocation))
self.log.debug("Adding tag %s to stack" % tag[0])
command = tagProperties.get('command', None)
originalAtts = tagProperties.get('originalAtts', None)
singletonTag = tagProperties.get('singletonTag', 0)
if (command is not None):
if (command[0] == METAL_USE_MACRO):
self.tagStack.append(
(tag, tagProperties, len(self.commandList) + 1))
else:
self.tagStack.append((tag, tagProperties, None))
else:
self.tagStack.append((tag, tagProperties, None))
if (command is not None):
# All tags that have a TAL attribute on them start with a 'start
# scope'
self.addCommand((TAL_START_SCOPE, (originalAtts, tag[1])))
# Now we add the TAL command
self.addCommand(command)
else:
# It's just a straight output, so create an output command and
# append it
self.addCommand((TAL_OUTPUT, self.tagAsText(tag, singletonTag)))
[docs] def popTag(self, tag, omitTagFlag=0):
""" omitTagFlag is used to control whether the end tag should be included in the
output or not. In HTML 4.01 there are several tags which should never have
end tags, this flag allows the template compiler to specify that these
should not be output.
"""
while self.tagStack:
oldTag, tagProperties, useMacroLocation = self.tagStack.pop()
endTagSymbol = tagProperties.get('endTagSymbol', None)
popCommandList = tagProperties.get('popFunctionList', [])
singletonTag = tagProperties.get('singletonTag', 0)
for func in popCommandList:
func()
self.log.debug("Popped tag %s off stack" % oldTag[0])
if (oldTag[0] == tag[0]):
# We've found the right tag, now check to see if we have any
# TAL commands on it
if (endTagSymbol is not None):
# We have a command (it's a TAL tag)
# Note where the end tag symbol should point (i.e. the next
# command)
self.symbolLocationTable[
endTagSymbol] = len(self.commandList)
# We need a "close scope and tag" command
self.addCommand(
(TAL_ENDTAG_ENDSCOPE, (tag[0], omitTagFlag, singletonTag)))
return
elif (omitTagFlag == 0 and singletonTag == 0):
# We are popping off an un-interesting tag, just add the
# close as text
self.addCommand((TAL_OUTPUT, '</' + tag[0] + '>'))
return
else:
# We are suppressing the output of this tag, so just return
return
else:
# We have a different tag, which means something like <br> which never closes is in
# between us and the real tag.
# If the tag that we did pop off has a command though it means
# un-balanced TAL tags!
if (endTagSymbol is not None):
# ERROR
msg = ("TAL/METAL Elements must be balanced - found close tag %s expecting %s"
% (tag[0], oldTag[0]))
self.log.error(msg)
raise TemplateParseException(self.tagAsText(oldTag), msg)
self.log.error(
"Close tag %s found with no corresponding open tag." % tag[0])
raise TemplateParseException(
"</%s>" % tag[0], "Close tag encountered with no corresponding open tag.")
[docs] def parseStartTag(self, tag, attributes, singletonElement=0):
# Note down the tag we are handling, it will be used for error handling during
# compilation
self.currentStartTag = (tag, attributes)
# Look for tal/metal attributes
foundTALAtts = []
foundMETALAtts = []
foundCommandsArgs = {}
cleanAttributes = []
originalAttributes = {}
tagProperties = {}
popTagFuncList = []
TALElementNameSpace = 0
prefixToAdd = ""
tagProperties['singletonTag'] = singletonElement
# Determine whether this element is in either the METAL or TAL
# namespace
if (tag.find(':') > 0):
# We have a namespace involved, so let's look to see if its one of
# ours
namespace = tag[0:tag.find(':')]
if (namespace == self.metal_namespace_prefix):
TALElementNameSpace = 1
prefixToAdd = self.metal_namespace_prefix + ":"
elif (namespace == self.tal_namespace_prefix):
TALElementNameSpace = 1
prefixToAdd = self.tal_namespace_prefix + ":"
if (TALElementNameSpace):
# We should treat this an implicit omit-tag
foundTALAtts.append(TAL_OMITTAG)
# Will go to default, i.e. yes
foundCommandsArgs[TAL_OMITTAG] = ""
for att, value in attributes:
originalAttributes[att] = value
if (TALElementNameSpace and not att.find(':') > 0):
# This means that the attribute name does not have a namespace,
# so use the prefix for this tag.
commandAttName = prefixToAdd + att
else:
commandAttName = att
self.log.debug("Command name is now %s" % commandAttName)
if (att[0:5] == "xmlns"):
# We have a namespace declaration.
prefix = att[6:]
if (value == METAL_NAME_URI):
# It's a METAL namespace declaration
if prefix:
self.metal_namespace_prefix_stack.append(
self.metal_namespace_prefix)
self.setMETALPrefix(prefix)
# We want this function called when the scope ends
popTagFuncList.append(self.popMETALNamespace)
else:
# We don't allow METAL/TAL to be declared as a default
msg = "Can not use METAL name space by default, a prefix must be provided."
raise TemplateParseException(
self.tagAsText(self.currentStartTag), msg)
elif (value == TAL_NAME_URI):
# TAL this time
if prefix:
self.tal_namespace_prefix_stack.append(
self.tal_namespace_prefix)
self.setTALPrefix(prefix)
# We want this function called when the scope ends
popTagFuncList.append(self.popTALNamespace)
else:
# We don't allow METAL/TAL to be declared as a default
msg = "Can not use TAL name space by default, a prefix must be provided."
raise TemplateParseException(
self.tagAsText(self.currentStartTag), msg)
else:
# It's nothing special, just an ordinary namespace
# declaration
cleanAttributes.append((att, value))
elif (commandAttName in self.tal_attribute_map):
# It's a TAL attribute
cmnd = self.tal_attribute_map[commandAttName]
if (cmnd == TAL_OMITTAG and TALElementNameSpace):
self.log.warning(
"Supressing omit-tag command present on TAL or METAL element")
else:
foundCommandsArgs[cmnd] = value
foundTALAtts.append(cmnd)
elif (commandAttName in self.metal_attribute_map):
# It's a METAL attribute
cmnd = self.metal_attribute_map[commandAttName]
foundCommandsArgs[cmnd] = value
foundMETALAtts.append(cmnd)
else:
cleanAttributes.append((att, value))
tagProperties['popFunctionList'] = popTagFuncList
# This might be just content
if not (foundTALAtts or foundMETALAtts):
# Just content, add it to the various stacks
self.addTag((tag, cleanAttributes), tagProperties)
return
# Create a symbol for the end of the tag - we don't know what the
# offset is yet
self.endTagSymbol += 1
tagProperties['endTagSymbol'] = self.endTagSymbol
# Sort the METAL commands
foundMETALAtts.sort()
# Sort the tags by priority
foundTALAtts.sort()
# We handle the METAL before the TAL
allCommands = foundMETALAtts + foundTALAtts
firstTag = 1
for talAtt in allCommands:
# Parse and create a command for each
cmnd = self.commandHandler[talAtt](foundCommandsArgs[talAtt])
if (cmnd is not None):
if (firstTag):
# The first one needs to add the tag
firstTag = 0
tagProperties['originalAtts'] = originalAttributes
tagProperties['command'] = cmnd
self.addTag((tag, cleanAttributes), tagProperties)
else:
# All others just append
self.addCommand(cmnd)
if (firstTag):
tagProperties['originalAtts'] = originalAttributes
tagProperties['command'] = (TAL_STARTTAG, (tag, singletonElement))
self.addTag((tag, cleanAttributes), tagProperties)
else:
# Add the start tag command in as a child of the last TAL command
self.addCommand((TAL_STARTTAG, (tag, singletonElement)))
[docs] def parseEndTag(self, tag):
""" Just pop the tag and related commands off the stack. """
self.popTag((tag, None))
[docs] def parseData(self, data):
# Just add it as an output
self.addCommand((TAL_OUTPUT, data))
[docs] def compileCmdDefine(self, argument):
# Compile a define command, resulting argument is:
# [(isLocalFlag (Y/n), variableName, variablePath),...]
# Break up the list of defines first
commandArgs = []
# We only want to match semi-colons that are not escaped
argumentSplitter = re.compile('(?<!;);(?!;)')
for defineStmt in argumentSplitter.split(argument):
# remove any leading space and un-escape any semi-colons
defineStmt = defineStmt.lstrip().replace(';;', ';')
# Break each defineStmt into pieces "[local|global] varName
# expression"
stmtBits = defineStmt.split(' ')
isLocal = 1
if (len(stmtBits) < 2):
# Error, badly formed define command
msg = ("Badly formed define command '%s'. Define commands must be of the form: '[local|global] varName expression[;[local|global] varName expression]'"
% argument)
self.log.error(msg)
raise TemplateParseException(
self.tagAsText(self.currentStartTag), msg)
# Assume to start with that >2 elements means a local|global flag
if (len(stmtBits) > 2):
if (stmtBits[0] == 'global'):
isLocal = 0
varName = stmtBits[1]
expression = ' '.join(stmtBits[2:])
elif (stmtBits[0] == 'local'):
varName = stmtBits[1]
expression = ' '.join(stmtBits[2:])
else:
# Must be a space in the expression that caused the >3
# thing
varName = stmtBits[0]
expression = ' '.join(stmtBits[1:])
else:
# Only two bits
varName = stmtBits[0]
expression = ' '.join(stmtBits[1:])
commandArgs.append((isLocal, varName, expression))
return (TAL_DEFINE, commandArgs)
[docs] def compileCmdCondition(self, argument):
# Compile a condition command, resulting argument is:
# path, endTagSymbol
# Sanity check
if not argument:
# No argument passed
msg = "No argument passed! condition commands must be of the form: 'path'"
self.log.error(msg)
raise TemplateParseException(
self.tagAsText(self.currentStartTag), msg)
return (TAL_CONDITION, (argument, self.endTagSymbol))
[docs] def compileCmdRepeat(self, argument):
# Compile a repeat command, resulting argument is:
# (varname, expression, endTagSymbol)
attProps = argument.split(' ')
if (len(attProps) < 2):
# Error, badly formed repeat command
msg = ("Badly formed repeat command '%s'. Repeat commands must be of the form: 'localVariable path'"
% argument)
self.log.error(msg)
raise TemplateParseException(
self.tagAsText(self.currentStartTag), msg)
varName = attProps[0]
expression = " ".join(attProps[1:])
return (TAL_REPEAT, (varName, expression, self.endTagSymbol))
[docs] def compileCmdContent(self, argument, replaceFlag=0):
# Compile a content command, resulting argument is
# (replaceFlag, structureFlag, expression, endTagSymbol)
# Sanity check
if not argument:
# No argument passed
msg = "No argument passed! content/replace commands must be of the form: 'path'"
self.log.error(msg)
raise TemplateParseException(
self.tagAsText(self.currentStartTag), msg)
structureFlag = 0
attProps = argument.split(' ')
if (len(attProps) > 1):
if (attProps[0] == "structure"):
structureFlag = 1
express = " ".join(attProps[1:])
elif (attProps[0] == "text"):
structureFlag = 0
express = " ".join(attProps[1:])
else:
# It's not a type selection after all - assume it's part of the
# path
express = argument
else:
express = argument
return (TAL_CONTENT, (replaceFlag, structureFlag, express, self.endTagSymbol))
[docs] def compileCmdReplace(self, argument):
return self.compileCmdContent(argument, replaceFlag=1)
[docs] def compileCmdAttributes(self, argument):
# 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('(?<!;);(?!;)')
for attributeStmt in argumentSplitter.split(argument):
# 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 TemplateParseException(
self.tagAsText(self.currentStartTag), msg)
attName = stmtBits[0]
attExpr = " ".join(stmtBits[1:])
commandArgs.append((attName, attExpr))
return (TAL_ATTRIBUTES, commandArgs)
[docs] def compileCmdOmitTag(self, argument):
# Compile a condition command, resulting argument is:
# path
# If no argument is given then set the path to default
if not argument:
expression = "default"
else:
expression = argument
return (TAL_OMITTAG, expression)
# METAL compilation commands go here
[docs]class TemplateParseException (Exception):
def __init__(self, location, errorDescription):
self.location = location
self.errorDescription = errorDescription
def __str__(self):
return "[" + self.location + "] " + self.errorDescription
[docs]class HTMLTemplateCompiler (TemplateCompiler, HTMLParser.HTMLParser):
def __init__(self):
TemplateCompiler.__init__(self)
HTMLParser.HTMLParser.__init__(self)
self.log = logging.getLogger("simpleTAL.HTMLTemplateCompiler")
[docs] def parseTemplate(self, file, encoding="UTF-8-SIG", minimizeBooleanAtts=0):
if isinstance(file, io.TextIOBase):
s = file.read()
else:
s = file.read().decode(encoding, 'replace')
self.encoding = encoding
self.minimizeBooleanAtts = minimizeBooleanAtts
self.feed(s)
self.close()
[docs] def tagAsText(self, tag_atts, singletonFlag=0):
""" This returns a tag as text.
"""
(tag, atts) = tag_atts
result = ["<"]
result.append(tag)
upperTag = tag.upper()
for attName, attValue in atts:
if (self.minimizeBooleanAtts and ((upperTag, attName.upper())) in HTML_BOOLEAN_ATTS):
# We should output a minimised boolean value
result.append(' ')
result.append(attName)
else:
result.append(' ')
result.append(attName)
result.append('=')
result.append(xml.sax.saxutils.quoteattr(attValue))
if (singletonFlag):
result.append(" />")
else:
result.append(">")
return "".join(result)
[docs] def handle_startendtag(self, tag, attributes):
self.log.debug("Recieved StartEnd Tag: " + tag +
" Attributes: " + unicode(attributes))
atts = []
for att, attValue in attributes:
# We need to spot empty tal:omit-tags
if (attValue is None):
if (att == self.tal_namespace_omittag):
atts.append((att, ""))
else:
atts.append((att, att))
else:
atts.append((att, attValue))
self.parseStartTag(tag, atts)
self.popTag((tag, None), omitTagFlag=1)
[docs] def handle_starttag(self, tag, attributes):
self.log.debug("Recieved Start Tag: " + tag +
" Attributes: " + unicode(attributes))
atts = []
for att, attValue in attributes:
# We need to spot empty tal:omit-tags
if (attValue is None):
if (att == self.tal_namespace_omittag):
atts.append((att, ""))
else:
atts.append((att, att))
else:
atts.append((att, attValue))
if (tag.upper() in HTML_FORBIDDEN_ENDTAG):
# This should have no end tag, so we just do the start and suppress
# the end
self.parseStartTag(tag, atts)
self.log.debug(
"End tag forbidden, generating close tag with no output.")
self.popTag((tag, None), omitTagFlag=1)
else:
self.parseStartTag(tag, atts)
[docs] def handle_endtag(self, tag):
self.log.debug("Recieved End Tag: " + tag)
if (tag.upper() in HTML_FORBIDDEN_ENDTAG):
self.log.warning(
"HTML forbids end tags for the %s element" % tag)
else:
# Normal end tag
self.popTag((tag, None))
[docs] def handle_data(self, data):
self.parseData(xml.sax.saxutils.escape(data))
# These two methods are required so that we expand all character and
# entity references prior to parsing the template.
[docs] def handle_charref(self, ref):
self.log.debug("Got Ref: %s", ref)
self.parseData(unichr(int(ref)))
[docs] def handle_entityref(self, ref):
self.log.debug("Got Ref: %s", ref)
# Use handle_data so that <&> are re-encoded as required.
self.handle_data(name2unicode.get(ref, '\ufffd'))
# Handle document type declarations
[docs] def handle_decl(self, data):
self.parseData('<!%s>' % data)
# Pass comments through un-affected.
[docs] def handle_pi(self, data):
self.log.debug("Recieved processing instruction.")
self.parseData('<?%s>' % data)
[docs] def report_unbalanced(self, tag):
self.log.warning("End tag %s present with no corresponding open tag.")
[docs] def getTemplate(self):
template = HTMLTemplate(self.commandList, self.macroMap,
self.symbolLocationTable, minimizeBooleanAtts=self.minimizeBooleanAtts)
return template
[docs]class XMLTemplateCompiler (TemplateCompiler, xml.sax.handler.ContentHandler, xml.sax.handler.DTDHandler, LexicalHandler):
def __init__(self):
TemplateCompiler.__init__(self)
xml.sax.handler.ContentHandler.__init__(self)
self.doctype = None
self.log = logging.getLogger("simpleTAL.XMLTemplateCompiler")
self.singletonElement = 0
[docs] def parseTemplate(self, file):
self.ourParser = xml.sax.make_parser()
self.log.debug("Setting features of parser")
try:
self.ourParser.setFeature(xml.sax.handler.feature_external_ges, 0)
except:
pass
if use_lexical_handler:
self.ourParser.setProperty(
xml.sax.handler.property_lexical_handler, self)
self.ourParser.setContentHandler(self)
self.ourParser.setDTDHandler(self)
self.ourParser.parse(file)
[docs] def parseDOM(self, dom):
if (not use_dom2sax):
self.log.critical("PyXML is not available, DOM can not be parsed.")
self.ourParser = xml.dom.ext.Dom2Sax.Dom2SaxParser()
self.log.debug("Setting features of parser")
if use_lexical_handler:
self.ourParser.setProperty(
xml.sax.handler.property_lexical_handler, self)
self.ourParser.setContentHandler(self)
self.ourParser.setDTDHandler(self)
self.ourParser.parse(dom)
[docs] def startDTD(self, name, public_id, system_id):
self.log.debug("Recieved DOCTYPE: " + name +
" public_id: " + public_id + " system_id: " + system_id)
if public_id:
self.doctype = '<!DOCTYPE %s PUBLIC "%s" "%s">' % (
name, public_id, system_id,)
else:
self.doctype = '<!DOCTYPE %s SYSTEM "%s">' % (name, system_id,)
[docs] def startElement(self, tag, attributes):
self.log.debug("Recieved Real Start Tag: " + tag +
" Attributes: " + unicode(attributes))
try:
xmlText = self.ourParser.getProperty(
xml.sax.handler.property_xml_string)
if ((isinstance(xmlText, unicode) and SINGLETON_XML_REGEX.match(xmlText.encode("utf-8")))
or SINGLETON_XML_REGEX.match(xmlText)):
# This is a singleton!
self.singletonElement = 1
except xml.sax.SAXException as e:
# Parser doesn't support this property
pass
# Convert attributes into a list of tuples
atts = []
for att in attributes.getNames():
self.log.debug("Attribute name %s has value %s" %
(att, attributes[att]))
atts.append((att, attributes[att]))
self.parseStartTag(tag, atts, singletonElement=self.singletonElement)
[docs] def endElement(self, tag):
self.log.debug("Recieved Real End Tag: " + tag)
self.parseEndTag(tag)
self.singletonElement = 0
[docs] def skippedEntity(self, name):
self.log.info("Recieved skipped entity: %s" % name)
self.characters(name2unicode.get(name, '\ufffd'))
[docs] def characters(self, data):
#self.log.debug ("Recieved Real Data: " + data)
# Escape any data we recieve - we don't want any: <&> in there.
self.parseData(xml.sax.saxutils.escape(data))
[docs] def processingInstruction(self, target, data):
self.log.debug("Recieved processing instruction.")
self.parseData('<?%s %s?>' % (target, data))
[docs] def getTemplate(self):
template = XMLTemplate(
self.commandList, self.macroMap, self.symbolLocationTable, self.doctype)
return template
[docs]def compileHTMLTemplate(template, inputEncoding="UTF-8-SIG", minimizeBooleanAtts=0):
""" Reads the templateFile and produces a compiled template.
To use the resulting template object call:
template.expand (context, outputFile)
"""
if isinstance(template, unicode):
# It's a string!
templateFile = io.StringIO(template)
elif isinstance(template, bytes):
templateFile = io.BytesIO(template)
else:
templateFile = template
compiler = HTMLTemplateCompiler()
compiler.parseTemplate(templateFile, inputEncoding, minimizeBooleanAtts)
return compiler.getTemplate()
[docs]def compileXMLTemplate(template):
""" Reads the templateFile and produces a compiled template.
To use the resulting template object call:
template.expand (context, outputFile)
"""
if isinstance(template, unicode):
# It's a string!
templateFile = io.StringIO(template)
elif (isinstance(template, bytes)):
# It's a string!
templateFile = io.BytesIO(template)
else:
templateFile = template
compiler = XMLTemplateCompiler()
compiler.parseTemplate(templateFile)
return compiler.getTemplate()
[docs]def compileDOMTemplate(template):
""" Traverses the DOM and produces a compiled template.
To use the resulting template object call:
template.expand (context, outputFile)
"""
compiler = XMLTemplateCompiler()
compiler.parseDOM(template)
return compiler.getTemplate()