File: taldom.py

package info (click to toggle)
python-jtoolkit 0.7.8-2
  • links: PTS
  • area: main
  • in suites: etch, etch-m68k
  • size: 1,436 kB
  • ctags: 2,536
  • sloc: python: 15,143; makefile: 20
file content (382 lines) | stat: -rw-r--r-- 15,090 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""implementation of TAL templating on the Python DOM libraries
if simpletal is installed, it will fall back to that (for possible performance gains)
provides required enhancements to the DOM libraries too, which are applied by importing this
"""

# Copyright 2005 St James Software
# 
# This file is part of jToolkit.
#
# jToolkit is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# jToolkit 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with jToolkit; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

from jToolkit.xml.fixminidom import minidom
import re
try:
  from simpletal import simpleTAL, simpleTALES
except ImportError:
  simpleTAL, simpleTALES = None, None
try:
  from cStringIO import StringIO
except ImportError:
  from StringIO import StringIO

class escaped(basestring):
  """some kind of string that has already been escaped and doesn't need to be again"""
  pass

class escapedunicode(unicode, escaped):
  """unicode that has already been escaped and doesn't need to be again"""
  pass

class escapedstr(str, escaped):
  """str that has already been escaped and doesn't need to be again"""
  pass

def format(values,formatstring):
  """helper function for formatting strings"""
  return formatstring%values
  
class DOMTemplate:
  def __init__(self, sourcedom):
    self.sourcedom = sourcedom
    self.contextstack = []
  def appendchildren(self,parentnode,childnode):
    """
    Adds children to the parentnode.
    If children is a list then it adds all of the items in the list
    """
    for node in childnode:
      if isinstance(node,tuple):
        parent,children = node
        parent = self.appendchildren(parent,children)
        parentnode.appendChild(parent)
      elif isinstance(node,list):
        self.appendchildren(parentnode,list)
      else:
        parentnode.appendChild(node)
    return parentnode

  def getcontext(self):
    """merges the levels of the context stack so that local 
    variables can override global variables only within their scope"""
    localcontext = {}
    for context in self.contextstack:
      localcontext.update(context)
    return localcontext
  
  def taleval(self, expr, context):
    """evaluates a tal expression using the context"""
    if expr.startswith("not "):
      result = not self.taleval(expr.replace("not ", "", 1), context)
      return result
    if expr.startswith("python:"):
      expression, formatting = self.parseformat(expr)
      if formatting is None:
        return None
      # expand the TALES path and perform evaluate the function
      value = self.taleval(expression, context)
      return format(value,formatting)
    if "/" in expr:
      parentvar, subexpr = expr.split("/", 1)
      if parentvar in context:
        return self.taleval(subexpr, context[parentvar])
      # print expr, "not found", repr(parentvar), parentvar in context, context.keys()
      return None
    if expr in context:
      return context[expr]
    return None

  def findtalattrs(self, srcnode=None):
    """returns a list of all the tal attributes defined"""
    if srcnode is None:
      srcnode = self.sourcedom
    attrs = []
    if isinstance(srcnode, minidom.Element):
      for attr in srcnode.attributes.values():
        if attr.nodeName.startswith("tal:"):
          attrs.append((srcnode.nodeName, attr.nodeName, attr.value))
    for srcchild in srcnode.childNodes:
      attrs += self.findtalattrs(srcchild)
    return attrs

  def findtalvarnames(self, srcnode=None):
    """returns a list of all the variable names expected"""
    expressions = []
    repeatvars = []
    for nodename, attrname, parameter in self.findtalattrs(srcnode):
      if attrname == "tal:content":
        expressions.append(parameter)
      elif attrname == "tal:repeat":
        repeatvar, repeatexpr = self.parserepeat(parameter)
        expressions.append(repeatexpr)
        repeatvars.append(repeatvar)
      elif attrname == "tal:attributes":
        for attrname, attrexpr in self.parseattributes(parameter):
          expressions.append(attrexpr)
    for expression in expressions:
      while expression.startswith("not "):
        expression = expression.replace("not ", "", 1)
      if expression.startswith("python:"):
        expression, format = self.parseformat(expression)
      if expression not in repeatvars:
        yield expression

  def parserepeat(self, parameter):
    """returns the repeat variable and repeat expression"""
    repeatvar, repeatexpr = parameter.split(" ", 1)
    return repeatvar, repeatexpr

  def parseattributes(self, parameter):
    """yields a list of attribute names and value expressions"""
    attributedefs = parameter.split(";")
    for attributedef in attributedefs:
      attrname, attrexpr = attributedef.strip().split(" ", 1)
      yield attrname, attrexpr

  def parseformat(self, parameter):
    """returns a (expression, format string) tuple"""
    if parameter.startswith("python:"):
      pythonpart = parameter.replace("python:", "", 1)
      match = re.match("^format\((.*),['](.*)[']\)$",pythonpart)
      if match:
        expression = match.group(1)
        formatting = match.group(2)
        return expression, formatting
    return None, None

  def sortoperations(self, a, b):
    """return whether a should be before or after b"""
    operations = ["condition", "define", "content", "repeat", "attributes", "named"]
    return operations.index(a[0]) - operations.index(b[0])
    
  def talexpand(self, srcnode, localize=None, forrepeatvar=None):
    """returns an expanded node using the context variables and any tal: attributes"""
    context = self.getcontext()
    contextframe = {"id":srcnode}
    self.contextstack.append(contextframe)
    newOwnerDocument = srcnode.ownerDocument   # will be changed when reattaching
    if isinstance(srcnode, minidom.DocumentType):
      # documenttypes can't be cloned
      targetnode = minidom.DocumentType(None)
      targetnode.name = srcnode.name
      targetnode.nodeName = srcnode.name
      srcnode._call_user_data_handler(minidom.xml.dom.UserDataHandler.NODE_CLONED, srcnode, targetnode)
      return targetnode
    includechildren = True
    if isinstance(srcnode, minidom.Element):
      targetnode = newOwnerDocument.createElementNS(srcnode.namespaceURI, srcnode.nodeName)
      taloperations = []
      operationspresent = {}
      standardattrs, derivedattrs = [], []
      repeatvar = None
      for attr in srcnode.attributes.values():
        if attr.nodeName.startswith("tal:"):
          operation = attr.nodeName.replace("tal:", "", 1)
          if operation in ("content", "repeat", "condition", "attributes", "named", "define"):
            taloperations.append((operation, attr.value))
            operationspresent[operation] = True
          else:
            print "unknown tal operation: %s in tag %r" % (operation, srcnode)
        else:
          standardattrs.append(attr)
      getparameters = lambda filterop: [parameter for operation, parameter in taloperations if operation == filterop]
      if "repeat" in operationspresent:
        for parameter in getparameters("repeat"):
          repeatvar, repeatexpr = self.parserepeat(parameter)
          if repeatvar == forrepeatvar:
            continue
          ## if repeatvar in context:
            ## raise ValueError("repeatvar %s already defined in context" % (repeatvar))
          repeatitems = self.taleval(repeatexpr, context)

          #repeatitems is a list of dictionaries
          repeatnodes = []
          for repeatvalue in repeatitems:
            contextframe[repeatvar] = repeatvalue
            repeatnodes.append(self.talexpand(srcnode, localize, repeatvar))

          self.contextstack.pop()
          return repeatnodes
      taloperations.sort(self.sortoperations)
      for operation, parameter in taloperations:
        if operation == "content":
          content = self.taleval(parameter, context)
          if content is not None:
            if isinstance(content, escaped):
              # this is awful. but its the easiest way of not escaping the data...
              contentdocument = "<xml>" + content + "</xml>"
              contentnode = minidom.parseString(contentdocument).firstChild
              for childnode in contentnode.childNodes:
                targetnode.appendChild(childnode.cloneNode(deep=True))
            elif isinstance(content,minidom.Element):
              contentnode = self.talexpand(content.cloneNode(deep=True))
              targetnode.appendChild(contentnode)
            else:
              contentnode = newOwnerDocument.createTextNode(content)
              targetnode.appendChild(contentnode)
          includechildren = False
        elif operation == "repeat":
          pass
        elif operation == "condition":
          condition = self.taleval(parameter, context)
          if not condition:
            self.contextstack.pop()
            return []
        elif operation == "attributes":
          for attrname, attrexpr in self.parseattributes(parameter):
            attrvalue = self.taleval(attrexpr, context)
            if attrvalue is not None:
              targetnode.setAttribute(attrname, attrvalue)
              derivedattrs.append(attrname)
        elif operation == "named":
          # Defines a named template in the global context
          self.contextstack[0][parameter] = srcnode
        elif operation == "define":
          # a tal variable is being defined:
          # argument       ::= define_scope [';' define_scope]*
          # define_scope   ::= (['local'] | 'global') define_var
          # define_var     ::= variable_name expression
          # variable_name  ::= Name          variable_name, expression = parameter.split(" ")
          if parameter.startswith('local'):
            scope = 'local'
            parameter = parameter.replace('local ','')
          elif parameter.startswith('global'):
            scope = 'global'
            parameter = parameter.replace('global ','')
          else:
            scope = 'local'
          variable_name,expression = parameter.split(' ')
          value = self.taleval(expression,context)
          if value:
            if scope == 'global':
              self.contextstack[0][variable_name] = value
            if scope == 'local':
              #local scope
              contextframe[variable_name] = value
      for attr in standardattrs:
        if not attr.nodeName in derivedattrs:
          targetnode.setAttributeNS(attr.namespaceURI, attr.nodeName, attr.value)
          a = targetnode.getAttributeNodeNS(attr.namespaceURI, attr.localName)
          a.specified = attr.specified
    elif isinstance(srcnode, minidom.Text):
      text = srcnode.data.strip()
      if text and localize is not None:
        textpos = srcnode.data.find(text)
        lwhitespace, rwhitespace = srcnode.data[:textpos], srcnode.data[textpos+len(text):]
        text = lwhitespace + localize(text) + rwhitespace
        # TODO: handle XML within the localization...
        targetnode = newOwnerDocument.createTextNode(text)
      else:
        targetnode = srcnode.cloneNode(deep=False)
    else:
      targetnode = srcnode.cloneNode(deep=False)
    if includechildren:
      for srcchild in srcnode.childNodes:
        targetchild = self.talexpand(srcchild, localize)
        if targetchild is None:
          break
        elif isinstance(targetchild, list):
          targetnode = self.appendchildren(targetnode,targetchild)
        else:
          targetnode.appendChild(targetchild)
    self.contextstack.pop()
    return targetnode

  def expand(self, context, innerid=None, localize=None):
    """expands the template (or a subtemplate using innerid) with the given context. localizes if localize given"""
    # put the global context into the context stack
    self.contextstack.append(context)
    if innerid is None:
      targetnode = minidom.Document()
      for srcchild in self.sourcedom.childNodes:
        targetchild = self.talexpand(srcchild, localize)
        if targetchild is not None:
          targetnode.appendChild(targetchild)
    else:
      srcnode = self.sourcedom.getElementById(innerid)
      targetnode = minidom.Document()
      targetchild = self.talexpand(srcnode, localize)
      targetnode.appendChild(targetchild)
    # print "nodetype", targetnode.nodeType, minidom.Node.DOCUMENT_NODE
    return targetnode.toxml("utf-8")

# alternative implementations using the functions here and the simpleTAL ones

# jToolkitTAL (this module) wrappers

class jToolkitTALContext(dict):
  def addGlobal(self, key, value):
    self[key] = value
  def evaluate(self, expr, originalAtts=None):
    return self[expr]

def jToolkitTALcompileXMLTemplate(source):
  sourcedom = minidom.parseString(source)
  return DOMTemplate(sourcedom)

# simpleTAL modifications

if simpleTAL is not None:
  XMLTemplate = simpleTAL.XMLTemplate
  class XMLTemplateWithInnerId(XMLTemplate):
      def expand(self, context, innerid=None):
          if innerid is not None:
              if innerid in self.innertemplates:
                  innertemplate = self.innertemplates[innerid]
              else:
                  innerelement = self.sourcedom.getElementById(innerid)
                  if innerelement:
                      innersource = innerelement.toxml().encode("UTF-8")
                      innertemplate = simpleTAL.compileXMLTemplate(innersource)
                      self.innertemplates[innerid] = innertemplate
                  else:
                      raise ValueError("innerid %s not found: %r" % (innerid, self.sourcedom._id_cache))
              return innertemplate.expand(context)
          outputbuf = StringIO()
          XMLTemplate.expand(self, context, outputbuf)
          return outputbuf.getvalue()
  simpleTAL.XMLTemplate = XMLTemplateWithInnerId

# simpleTAL wrappers

def simpleTALcompileXMLTemplate(source):
  sourcedom = minidom.parseString(source)
  template = simpleTAL.compileXMLTemplate(source)
  template.sourcedom = sourcedom
  template.innertemplates = {}
  return template

# switch implementations

implementation = None

def usesimpleTAL():
  global Context, compileXMLTemplate, implementation
  Context = simpleTALES.Context
  compileXMLTemplate = simpleTALcompileXMLTemplate
  implementation = "simpleTAL"

def usejToolkitTAL():
  global Context, compileXMLTemplate, implementation
  Context = jToolkitTALContext
  compileXMLTemplate = jToolkitTALcompileXMLTemplate
  implementation = "jToolkitTAL"

# use jToolkit TAL by default
usejToolkitTAL()