TALGenerator.py 20.2 KB
Newer Older
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
##############################################################################
# 
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
# 
# Copyright (c) Digital Creations.  All rights reserved.
# 
# This license has been certified as Open Source(tm).
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# 
# 1. Redistributions in 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. Digital Creations requests that attribution be given to Zope
#    in any manner possible. Zope includes a "Powered by Zope"
#    button that is installed by default. While it is not a license
#    violation to remove this button, it is requested that the
#    attribution remain. A significant investment has been put
#    into Zope, and this effort will continue if the Zope community
#    continues to grow. This is one way to assure that growth.
# 
# 4. All advertising materials and documentation mentioning
#    features derived from or use of this software must display
#    the following acknowledgement:
# 
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
# 
#    In the event that the product being advertised includes an
#    intact Zope distribution (with copyright and license included)
#    then this clause is waived.
# 
# 5. Names associated with Zope or Digital Creations must not be used to
#    endorse or promote products derived from this software without
#    prior written permission from Digital Creations.
# 
# 6. Modified redistributions of any form whatsoever must retain
#    the following acknowledgment:
# 
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
# 
#    Intact (re-)distributions of any official Zope release do not
#    require an external acknowledgement.
# 
# 7. Modifications are encouraged but must be packaged separately as
#    patches to official Zope releases.  Distributions that do not
#    clearly separate the patches from the original work must be clearly
#    labeled as unofficial distributions.  Modifications which do not
#    carry the name Zope may be packaged in any form, as long as they
#    conform to all of the clauses above.
# 
# 
# Disclaimer
# 
#   THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
#   EXPRESSED 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 DIGITAL CREATIONS OR ITS
#   CONTRIBUTORS 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.
# 
# 
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations.  Specific
# attributions are listed in the accompanying credits file.
# 
##############################################################################
"""
Code generator for TALInterpreter intermediate code.
"""

89
import string
90 91 92 93 94 95 96
import re
import cgi

from TALDefs import *

class TALGenerator:

97
    def __init__(self, expressionCompiler=None, xml=1):
98
        if not expressionCompiler:
99
            from DummyEngine import DummyEngine
100
            expressionCompiler = DummyEngine()
101 102 103
        self.expressionCompiler = expressionCompiler
        self.program = []
        self.stack = []
104
        self.todoStack = []
105 106 107
        self.macros = {}
        self.slots = {}
        self.slotStack = []
108
        self.xml = xml
109
        self.emit("version", TAL_VERSION)
110
        self.emit("mode", xml and "xml" or "html")
111

112
    def getCode(self):
113 114
        assert not self.stack
        assert not self.todoStack
115 116 117
        return self.optimize(self.program), self.macros

    def optimize(self, program):
118 119 120
        output = []
        collect = []
        rawseen = cursor = 0
121 122 123 124
        if self.xml:
            endsep = "/>"
        else:
            endsep = " />"
125 126 127 128 129
        for cursor in xrange(len(program)+1):
            try:
                item = program[cursor]
            except IndexError:
                item = (None, None)
Fred Drake's avatar
Fred Drake committed
130 131
            opcode = item[0]
            if opcode == "rawtext":
132 133
                collect.append(item[1])
                continue
Fred Drake's avatar
Fred Drake committed
134
            if opcode == "endTag":
135 136
                collect.append("</%s>" % item[1])
                continue
Fred Drake's avatar
Fred Drake committed
137
            if opcode == "startTag":
138 139
                if self.optimizeStartTag(collect, item[1], item[2], ">"):
                    continue
Fred Drake's avatar
Fred Drake committed
140
            if opcode == "startEndTag":
141
                if self.optimizeStartTag(collect, item[1], item[2], endsep):
142
                    continue
143 144 145 146 147 148
            if opcode in ("beginScope", "endScope"):
                # Push *Scope instructions in front of any text instructions;
                # this allows text instructions separated only by *Scope
                # instructions to be joined together.
                output.append(self.optimizeArgsList(item))
                continue
149 150
            text = string.join(collect, "")
            if text:
Fred Drake's avatar
Fred Drake committed
151 152 153 154 155 156 157
                i = string.rfind(text, "\n")
                if i >= 0:
                    i = len(text) - (i + 1)
                    output.append(("rawtextColumn", (text, i)))
                else:
                    output.append(("rawtextOffset", (text, len(text))))
            if opcode != None:
158
                output.append(self.optimizeArgsList(item))
159 160
            rawseen = cursor+1
            collect = []
161
        return self.optimizeCommonTriple(output)
162

163 164 165 166 167 168
    def optimizeArgsList(self, item):
        if len(item) == 2:
            return item
        else:
            return item[0], tuple(item[1:])

Fred Drake's avatar
Fred Drake committed
169 170
    actionIndex = {"replace":0, "insert":1, "metal":2, "tal":3, "xmlns":4,
                   0: 0, 1: 1, 2: 2, 3: 3, 4: 4}
171 172 173 174
    def optimizeStartTag(self, collect, name, attrlist, end):
        if not attrlist:
            collect.append("<%s%s" % (name, end))
            return 1
175
        opt = 1
176
        new = ["<" + name]
177 178
        for i in range(len(attrlist)):
            item = attrlist[i]
179
            if len(item) > 2:
180
                opt = 0
Fred Drake's avatar
Fred Drake committed
181 182 183
                name, value, action = item[:3]
                action = self.actionIndex[action]
                attrlist[i] = (name, value, action) + item[3:]
184 185 186 187 188 189
            else:
                if item[1] is None:
                    s = item[0]
                else:
                    s = "%s=%s" % (item[0], quote(item[1]))
                attrlist[i] = item[0], s
190
            if item[1] is None:
191
                new.append(" " + item[0])
192 193
            else:
                new.append(" %s=%s" % (item[0], quote(item[1])))
194 195 196 197
        if opt:
            new.append(end)
            collect.extend(new)
        return opt
198

199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
    def optimizeCommonTriple(self, program):
        if len(program) < 3:
            return program
        output = program[:2]
        prev2, prev1 = output
        for item in program[2:]:
            if (  item[0] == "beginScope"
                  and prev1[0] == "setPosition"
                  and prev2[0] == "rawtextColumn"):
                position = output.pop()[1]
                text, column = output.pop()[1]
                prev1 = None, None
                closeprev = 0
                if output and output[-1][0] == "endScope":
                    closeprev = 1
                    output.pop()
                item = ("rawtextBeginScope",
                        (text, column, position, closeprev, item[1]))
            output.append(item)
            prev2 = prev1
            prev1 = item
        return output

222 223 224 225 226 227
    def todoPush(self, todo):
        self.todoStack.append(todo)

    def todoPop(self):
        return self.todoStack.pop()

228 229 230 231 232 233 234 235 236 237
    def compileExpression(self, expr):
        return self.expressionCompiler.compile(expr)

    def pushProgram(self):
        self.stack.append(self.program)
        self.program = []

    def popProgram(self):
        program = self.program
        self.program = self.stack.pop()
238
        return self.optimize(program)
239

240 241 242 243 244 245 246 247 248
    def pushSlots(self):
        self.slotStack.append(self.slots)
        self.slots = {}

    def popSlots(self):
        slots = self.slots
        self.slots = self.slotStack.pop()
        return slots

249 250 251
    def emit(self, *instruction):
        self.program.append(instruction)

252 253 254 255 256
    def emitStartTag(self, name, attrlist, isend=0):
        if isend:
            opcode = "startEndTag"
        else:
            opcode = "startTag"
257
        self.emit(opcode, name, attrlist)
258 259

    def emitEndTag(self, name):
260
        if self.xml and self.program and self.program[-1][0] == "startTag":
261 262 263
            # Minimize empty element
            self.program[-1] = ("startEndTag",) + self.program[-1][1:]
        else:
264
            self.emit("endTag", name)
265

266
    def emitRawText(self, text):
267
        self.emit("rawtext", text)
268

269 270 271
    def emitText(self, text):
        self.emitRawText(cgi.escape(text))

272
    def emitDefines(self, defines, position):
273 274
        for part in splitParts(defines):
            m = re.match(
275
                r"(?s)\s*(?:(global|local)\s+)?(%s)\s+(.*)\Z" % NAME_RE, part)
276
            if not m:
277
                raise TALError("invalid define syntax: " + `part`, position)
278 279 280 281 282 283 284 285
            scope, name, expr = m.group(1, 2, 3)
            scope = scope or "local"
            cexpr = self.compileExpression(expr)
            if scope == "local":
                self.emit("setLocal", name, cexpr)
            else:
                self.emit("setGlobal", name, cexpr)

286 287 288 289 290 291 292 293
    def emitOnError(self, name, onError, position):
        block = self.popProgram()
        key, expr = parseSubstitution(onError, position)
        cexpr = self.compileExpression(expr)
        if key == "text":
            self.emit("insertText", cexpr, [])
        else:
            assert key == "structure"
294
            self.emit("insertStructure", cexpr, {}, [])
295 296 297 298
        self.emitEndTag(name)
        handler = self.popProgram()
        self.emit("onError", block, handler)

299 300 301 302 303
    def emitCondition(self, expr):
        cexpr = self.compileExpression(expr)
        program = self.popProgram()
        self.emit("condition", cexpr, program)

304
    def emitRepeat(self, arg, position=(None, None)):
305
        m = re.match("(?s)\s*(%s)\s+(.*)\Z" % NAME_RE, arg)
306
        if not m:
307
            raise TALError("invalid repeat syntax: " + `arg`, position)
308 309 310 311 312
        name, expr = m.group(1, 2)
        cexpr = self.compileExpression(expr)
        program = self.popProgram()
        self.emit("loop", name, cexpr, program)

313
    def emitSubstitution(self, arg, attrDict={}, position=(None, None)):
314
        key, expr = parseSubstitution(arg, position)
315 316 317 318 319 320 321
        cexpr = self.compileExpression(expr)
        program = self.popProgram()
        if key == "text":
            self.emit("insertText", cexpr, program)
        else:
            assert key == "structure"
            self.emit("insertStructure", cexpr, attrDict, program)
322

323
    def emitDefineMacro(self, macroName, position=(None, None)):
324 325
        program = self.popProgram()
        if self.macros.has_key(macroName):
326 327
            raise METALError("duplicate macro definition: %s" % macroName,
                             position)
328 329 330 331 332 333
        self.macros[macroName] = program
        self.emit("defineMacro", macroName, program)

    def emitUseMacro(self, expr):
        cexpr = self.compileExpression(expr)
        program = self.popProgram()
334
        self.emit("useMacro", expr, cexpr, self.popSlots(), program)
335 336 337 338 339

    def emitDefineSlot(self, slotName):
        program = self.popProgram()
        self.emit("defineSlot", slotName, program)

340
    def emitFillSlot(self, slotName, position=(None, None)):
341 342
        program = self.popProgram()
        if self.slots.has_key(slotName):
343
            raise METALError("duplicate fill-slot name: %s" % slotName,
344
                             position)
345 346
        self.slots[slotName] = program
        self.emit("fillSlot", slotName, program)
347

348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
    def unEmitWhitespace(self):
        collect = []
        i = len(self.program) - 1
        while i >= 0:
            item = self.program[i]
            if item[0] != "rawtext":
                break
            text = item[1]
            if not re.match(r"\A\s*\Z", text):
                break
            collect.append(text)
            i = i-1
        del self.program[i+1:]
        if i >= 0 and self.program[i][0] == "rawtext":
            text = self.program[i][1]
            m = re.search(r"\s+\Z", text)
            if m:
                self.program[i] = ("rawtext", text[:m.start()])
                collect.append(m.group())
        collect.reverse()
        return string.join(collect, "")

370
    def unEmitNewlineWhitespace(self):
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389
        collect = []
        i = len(self.program)
        while i > 0:
            i = i-1
            item = self.program[i]
            if item[0] != "rawtext":
                break
            text = item[1]
            if re.match(r"\A[ \t]*\Z", text):
                collect.append(text)
                continue
            m = re.match(r"(?s)^(.*)(\n[ \t]*)\Z", text)
            if not m:
                break
            text, rest = m.group(1, 2)
            collect.reverse()
            rest = rest + string.join(collect, "")
            del self.program[i:]
            if text:
390
                self.emit("rawtext", text)
391
            return rest
392 393
        return None

394 395 396 397 398 399 400 401 402 403 404
    def replaceAttrs(self, attrlist, repldict):
        if not repldict:
            return attrlist
        newlist = []
        for item in attrlist:
            key = item[0]
            if repldict.has_key(key):
                item = item[:2] + ("replace", repldict[key])
                del repldict[key]
            newlist.append(item)
        for key, value in repldict.items(): # Add dynamic-only attributes
Evan Simpson's avatar
Evan Simpson committed
405
            item = (key, None, "insert", value)
406 407 408
            newlist.append(item)
        return newlist

409
    def emitStartElement(self, name, attrlist, taldict, metaldict,
410
                         position=(None, None), isend=0):
411
        if not taldict and not metaldict:
412 413 414 415 416 417
            # Handle the simple, common case
            self.emitStartTag(name, attrlist, isend)
            self.todoPush({})
            if isend:
                self.emitEndElement(name, isend)
            return
418

419 420
        for key in taldict.keys():
            if key not in KNOWN_TAL_ATTRIBUTES:
421
                raise TALError("bad TAL attribute: " + `key`, position)
422 423
        for key in metaldict.keys():
            if key not in KNOWN_METAL_ATTRIBUTES:
424
                raise METALError("bad METAL attribute: " + `key`, position)
425 426 427 428 429
        todo = {}
        defineMacro = metaldict.get("define-macro")
        useMacro = metaldict.get("use-macro")
        defineSlot = metaldict.get("define-slot")
        fillSlot = metaldict.get("fill-slot")
430
        define = taldict.get("define")
431
        condition = taldict.get("condition")
432
        repeat = taldict.get("repeat")
433
        content = taldict.get("content")
434 435
        replace = taldict.get("replace")
        attrsubst = taldict.get("attributes")
436
        onError = taldict.get("on-error")
437 438 439
        if len(metaldict) > 1 and (defineMacro or useMacro):
            raise METALError("define-macro and use-macro cannot be used "
                             "together or with define-slot or fill-slot",
440
                             position)
441 442
        if content and replace:
            raise TALError("content and replace are mutually exclusive",
443
                           position)
444

445 446 447 448
        repeatWhitespace = None
        if repeat:
            # Hack to include preceding whitespace in the loop program
            repeatWhitespace = self.unEmitNewlineWhitespace()
449 450 451
        if position != (None, None):
            # XXX at some point we should insist on a non-trivial position
            self.emit("setPosition", position)
452 453
        if defineMacro:
            self.pushProgram()
454 455
            self.emit("version", TAL_VERSION)
            self.emit("mode", self.xml and "xml" or "html")
456 457 458 459 460 461 462 463
            todo["defineMacro"] = defineMacro
        if useMacro:
            self.pushSlots()
            self.pushProgram()
            todo["useMacro"] = useMacro
        if fillSlot:
            self.pushProgram()
            todo["fillSlot"] = fillSlot
464 465 466
        if defineSlot:
            self.pushProgram()
            todo["defineSlot"] = defineSlot
467
        if taldict:
468 469 470 471 472
            dict = {}
            for item in attrlist:
                key, value = item[:2]
                dict[key] = value
            self.emit("beginScope", dict)
473
            todo["scope"] = 1
474 475 476 477 478 479 480 481
        if onError:
            self.pushProgram() # handler
            self.emitStartTag(name, attrlist)
            self.pushProgram() # block
            todo["onError"] = onError
        if define:
            self.emitDefines(define, position)
            todo["define"] = define
482 483 484
        if condition:
            self.pushProgram()
            todo["condition"] = condition
485
        if repeat:
486 487 488 489
            todo["repeat"] = repeat
            self.pushProgram()
            if repeatWhitespace:
                self.emitText(repeatWhitespace)
490 491 492 493 494
        if content:
            todo["content"] = content
        if replace:
            todo["replace"] = replace
            self.pushProgram()
495 496
        if attrsubst:
            repldict = parseAttributeReplacements(attrsubst)
497
            for key, value in repldict.items():
498
                repldict[key] = self.compileExpression(value)
499 500
        else:
            repldict = {}
501 502 503
        if replace:
            todo["repldict"] = repldict
            repldict = {}
504
        self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend)
505
        if content:
506
            self.pushProgram()
507 508
        if todo and position != (None, None):
            todo["position"] = position
509
        self.todoPush(todo)
510 511
        if isend:
            self.emitEndElement(name, isend)
512

513
    def emitEndElement(self, name, isend=0, implied=0):
514 515 516
        todo = self.todoPop()
        if not todo:
            # Shortcut
517 518
            if not isend:
                self.emitEndTag(name)
519
            return
520 521 522 523 524 525

        position = todo.get("position", (None, None))
        defineMacro = todo.get("defineMacro")
        useMacro = todo.get("useMacro")
        defineSlot = todo.get("defineSlot")
        fillSlot = todo.get("fillSlot")
526
        repeat = todo.get("repeat")
527
        content = todo.get("content")
528 529
        replace = todo.get("replace")
        condition = todo.get("condition")
530
        onError = todo.get("onError")
531 532
        define = todo.get("define")
        repldict = todo.get("repldict", {})
533
        scope = todo.get("scope")
534

535 536 537 538 539 540 541 542 543 544
        if implied > 0:
            if defineMacro or useMacro or defineSlot or fillSlot:
                exc = METALError
                what = "METAL"
            else:
                exc = TALError
                what = "TAL"
            raise exc("%s attributes on <%s> require explicit </%s>" %
                      (what, name, name), position)

545 546 547 548
        if content:
            self.emitSubstitution(content, {}, position)
        if not isend:
            self.emitEndTag(name)
549 550
        if replace:
            self.emitSubstitution(replace, repldict, position)
551
        if repeat:
552
            self.emitRepeat(repeat, position)
553 554
        if condition:
            self.emitCondition(condition)
555 556
        if onError:
            self.emitOnError(name, onError, position)
557
        if scope:
558 559 560 561
            self.emit("endScope")
        if defineSlot:
            self.emitDefineSlot(defineSlot)
        if fillSlot:
562
            self.emitFillSlot(fillSlot, position)
563 564 565 566
        if useMacro:
            self.emitUseMacro(useMacro)
        if defineMacro:
            self.emitDefineMacro(defineMacro, position)
567

568 569 570 571 572 573 574 575 576
def test():
    t = TALGenerator()
    t.pushProgram()
    t.emit("bar")
    p = t.popProgram()
    t.emit("foo", p)

if __name__ == "__main__":
    test()