Mercurial > p > roundup > code
diff roundup/cgi/TAL/TALGenerator.py @ 2348:8c2402a78bb0
beginning getting ZPT up to date: TAL first
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Fri, 21 May 2004 05:36:30 +0000 |
| parents | fc52d57c6c3e |
| children | 198b6e810c67 |
line wrap: on
line diff
--- a/roundup/cgi/TAL/TALGenerator.py Thu May 20 23:16:58 2004 +0000 +++ b/roundup/cgi/TAL/TALGenerator.py Fri May 21 05:36:30 2004 +0000 @@ -2,39 +2,57 @@ # # Copyright (c) 2001, 2002 Zope Corporation and Contributors. # All Rights Reserved. -# +# # This software is subject to the provisions of the Zope Public License, # Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# +# FOR A PARTICULAR PURPOSE. +# ############################################################################## -"""Code generator for TALInterpreter intermediate code. """ -__docformat__ = 'restructuredtext' +Code generator for TALInterpreter intermediate code. +""" -import string import re import cgi -from TALDefs import * +import TALDefs + +from TALDefs import NAME_RE, TAL_VERSION +from TALDefs import I18NError, METALError, TALError +from TALDefs import parseSubstitution +from TranslationContext import TranslationContext, DEFAULT_DOMAIN + +I18N_REPLACE = 1 +I18N_CONTENT = 2 +I18N_EXPRESSION = 3 + +_name_rx = re.compile(NAME_RE) + class TALGenerator: inMacroUse = 0 inMacroDef = 0 source_file = None - + def __init__(self, expressionCompiler=None, xml=1, source_file=None): if not expressionCompiler: from DummyEngine import DummyEngine expressionCompiler = DummyEngine() self.expressionCompiler = expressionCompiler self.CompilerError = expressionCompiler.getCompilerError() + # This holds the emitted opcodes representing the input self.program = [] + # The program stack for when we need to do some sub-evaluation for an + # intermediate result. E.g. in an i18n:name tag for which the + # contents describe the ${name} value. self.stack = [] + # Another stack of postponed actions. Elements on this stack are a + # dictionary; key/values contain useful information that + # emitEndElement needs to finish its calculations self.todoStack = [] self.macros = {} self.slots = {} @@ -45,6 +63,8 @@ if source_file is not None: self.source_file = source_file self.emit("setSourceFile", source_file) + self.i18nContext = TranslationContext() + self.i18nLevel = 0 def getCode(self): assert not self.stack @@ -54,7 +74,7 @@ def optimize(self, program): output = [] collect = [] - rawseen = cursor = 0 + cursor = 0 if self.xml: endsep = "/>" else: @@ -83,9 +103,15 @@ # instructions to be joined together. output.append(self.optimizeArgsList(item)) continue - text = string.join(collect, "") + if opcode == 'noop': + # This is a spacer for end tags in the face of i18n:name + # attributes. We can't let the optimizer collect immediately + # following end tags into the same rawtextOffset. + opcode = None + pass + text = "".join(collect) if text: - i = string.rfind(text, "\n") + i = text.rfind("\n") if i >= 0: i = len(text) - (i + 1) output.append(("rawtextColumn", (text, i))) @@ -93,7 +119,6 @@ output.append(("rawtextOffset", (text, len(text)))) if opcode != None: output.append(self.optimizeArgsList(item)) - rawseen = cursor+1 collect = [] return self.optimizeCommonTriple(output) @@ -103,9 +128,28 @@ else: return item[0], tuple(item[1:]) - actionIndex = {"replace":0, "insert":1, "metal":2, "tal":3, "xmlns":4, - 0: 0, 1: 1, 2: 2, 3: 3, 4: 4} + # These codes are used to indicate what sort of special actions + # are needed for each special attribute. (Simple attributes don't + # get action codes.) + # + # The special actions (which are modal) are handled by + # TALInterpreter.attrAction() and .attrAction_tal(). + # + # Each attribute is represented by a tuple: + # + # (name, value) -- a simple name/value pair, with + # no special processing + # + # (name, value, action, *extra) -- attribute with special + # processing needs, action is a + # code that indicates which + # branch to take, and *extra + # contains additional, + # action-specific information + # needed by the processing + # def optimizeStartTag(self, collect, name, attrlist, end): + # return true if the tag can be converted to plain text if not attrlist: collect.append("<%s%s" % (name, end)) return 1 @@ -116,18 +160,15 @@ if len(item) > 2: opt = 0 name, value, action = item[:3] - action = self.actionIndex[action] attrlist[i] = (name, value, action) + item[3:] else: if item[1] is None: s = item[0] else: - s = "%s=%s" % (item[0], quote(item[1])) + s = '%s="%s"' % (item[0], TALDefs.attrEscape(item[1])) attrlist[i] = item[0], s - if item[1] is None: - new.append(" " + item[0]) - else: - new.append(" %s=%s" % (item[0], quote(item[1]))) + new.append(" " + s) + # if no non-optimizable attributes were found, convert to plain text if opt: new.append(end) collect.extend(new) @@ -139,9 +180,9 @@ output = program[:2] prev2, prev1 = output for item in program[2:]: - if ( item[0] == "beginScope" - and prev1[0] == "setPosition" - and prev2[0] == "rawtextColumn"): + if ( item[0] == "beginScope" + and prev1[0] == "setPosition" + and prev2[0] == "rawtextColumn"): position = output.pop()[1] text, column = output.pop()[1] prev1 = None, None @@ -215,7 +256,7 @@ if cexpr: cexpr = self.compileExpression(optTag[0]) self.emit("optTag", name, cexpr, optTag[1], isend, start, program) - + def emitRawText(self, text): self.emit("rawtext", text) @@ -223,7 +264,7 @@ self.emitRawText(cgi.escape(text)) def emitDefines(self, defines): - for part in splitParts(defines): + for part in TALDefs.splitParts(defines): m = re.match( r"(?s)\s*(?:(global|local)\s+)?(%s)\s+(.*)\Z" % NAME_RE, part) if not m: @@ -278,9 +319,56 @@ assert key == "structure" self.emit("insertStructure", cexpr, attrDict, program) + def emitI18nVariable(self, stuff): + # Used for i18n:name attributes. arg is extra information describing + # how the contents of the variable should get filled in, and it will + # either be a 1-tuple or a 2-tuple. If arg[0] is None, then the + # i18n:name value is taken implicitly from the contents of the tag, + # e.g. "I live in <span i18n:name="country">the USA</span>". In this + # case, arg[1] is the opcode sub-program describing the contents of + # the tag. + # + # When arg[0] is not None, it contains the tal expression used to + # calculate the contents of the variable, e.g. + # "I live in <span i18n:name="country" + # tal:replace="here/countryOfOrigin" />" + varname, action, expression = stuff + m = _name_rx.match(varname) + if m is None or m.group() != varname: + raise TALError("illegal i18n:name: %r" % varname, self.position) + key = cexpr = None + program = self.popProgram() + if action == I18N_REPLACE: + # This is a tag with an i18n:name and a tal:replace (implicit or + # explicit). Get rid of the first and last elements of the + # program, which are the start and end tag opcodes of the tag. + program = program[1:-1] + elif action == I18N_CONTENT: + # This is a tag with an i18n:name and a tal:content + # (explicit-only). Keep the first and last elements of the + # program, so we keep the start and end tag output. + pass + else: + assert action == I18N_EXPRESSION + key, expr = parseSubstitution(expression) + cexpr = self.compileExpression(expr) + # XXX Would key be anything but 'text' or None? + assert key in ('text', None) + self.emit('i18nVariable', varname, program, cexpr) + + def emitTranslation(self, msgid, i18ndata): + program = self.popProgram() + if i18ndata is None: + self.emit('insertTranslation', msgid, program) + else: + key, expr = parseSubstitution(i18ndata) + cexpr = self.compileExpression(expr) + assert key == 'text' + self.emit('insertTranslation', msgid, program, cexpr) + def emitDefineMacro(self, macroName): program = self.popProgram() - macroName = string.strip(macroName) + macroName = macroName.strip() if self.macros.has_key(macroName): raise METALError("duplicate macro definition: %s" % `macroName`, self.position) @@ -299,7 +387,7 @@ def emitDefineSlot(self, slotName): program = self.popProgram() - slotName = string.strip(slotName) + slotName = slotName.strip() if not re.match('%s$' % NAME_RE, slotName): raise METALError("invalid slot name: %s" % `slotName`, self.position) @@ -307,7 +395,7 @@ def emitFillSlot(self, slotName): program = self.popProgram() - slotName = string.strip(slotName) + slotName = slotName.strip() if self.slots.has_key(slotName): raise METALError("duplicate fill-slot name: %s" % `slotName`, self.position) @@ -338,7 +426,7 @@ self.program[i] = ("rawtext", text[:m.start()]) collect.append(m.group()) collect.reverse() - return string.join(collect, "") + return "".join(collect) def unEmitNewlineWhitespace(self): collect = [] @@ -357,7 +445,7 @@ break text, rest = m.group(1, 2) collect.reverse() - rest = rest + string.join(collect, "") + rest = rest + "".join(collect) del self.program[i:] if text: self.emit("rawtext", text) @@ -365,23 +453,30 @@ return None def replaceAttrs(self, attrlist, repldict): + # Each entry in attrlist starts like (name, value). + # Result is (name, value, action, expr, xlat) if there is a + # tal:attributes entry for that attribute. Additional attrs + # defined only by tal:attributes are added here. + # + # (name, value, action, expr, xlat) if not repldict: return attrlist newlist = [] for item in attrlist: key = item[0] if repldict.has_key(key): - item = item[:2] + ("replace", repldict[key]) + expr, xlat, msgid = repldict[key] + item = item[:2] + ("replace", expr, xlat, msgid) del repldict[key] newlist.append(item) - for key, value in repldict.items(): # Add dynamic-only attributes - item = (key, None, "insert", value) - newlist.append(item) + # Add dynamic-only attributes + for key, (expr, xlat, msgid) in repldict.items(): + newlist.append((key, None, "insert", expr, xlat, msgid)) return newlist - def emitStartElement(self, name, attrlist, taldict, metaldict, + def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict, position=(None, None), isend=0): - if not taldict and not metaldict: + if not taldict and not metaldict and not i18ndict: # Handle the simple, common case self.emitStartTag(name, attrlist, isend) self.todoPush({}) @@ -391,18 +486,24 @@ self.position = position for key, value in taldict.items(): - if key not in KNOWN_TAL_ATTRIBUTES: + if key not in TALDefs.KNOWN_TAL_ATTRIBUTES: raise TALError("bad TAL attribute: " + `key`, position) if not (value or key == 'omit-tag'): raise TALError("missing value for TAL attribute: " + `key`, position) for key, value in metaldict.items(): - if key not in KNOWN_METAL_ATTRIBUTES: + if key not in TALDefs.KNOWN_METAL_ATTRIBUTES: raise METALError("bad METAL attribute: " + `key`, - position) + position) if not value: raise TALError("missing value for METAL attribute: " + `key`, position) + for key, value in i18ndict.items(): + if key not in TALDefs.KNOWN_I18N_ATTRIBUTES: + raise I18NError("bad i18n attribute: " + `key`, position) + if not value and key in ("attributes", "data", "id"): + raise I18NError("missing value for i18n attribute: " + + `key`, position) todo = {} defineMacro = metaldict.get("define-macro") useMacro = metaldict.get("use-macro") @@ -417,13 +518,36 @@ onError = taldict.get("on-error") omitTag = taldict.get("omit-tag") TALtag = taldict.get("tal tag") + i18nattrs = i18ndict.get("attributes") + # Preserve empty string if implicit msgids are used. We'll generate + # code with the msgid='' and calculate the right implicit msgid during + # interpretation phase. + msgid = i18ndict.get("translate") + varname = i18ndict.get('name') + i18ndata = i18ndict.get('data') + + if varname and not self.i18nLevel: + raise I18NError( + "i18n:name can only occur inside a translation unit", + position) + + if i18ndata and not msgid: + raise I18NError("i18n:data must be accompanied by i18n:translate", + position) + 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", position) - if content and replace: - raise TALError("content and replace are mutually exclusive", - position) + if replace: + if content: + raise TALError( + "tal:content and tal:replace are mutually exclusive", + position) + if msgid is not None: + raise I18NError( + "i18n:translate and tal:replace are mutually exclusive", + position) repeatWhitespace = None if repeat: @@ -441,8 +565,8 @@ self.inMacroUse = 0 else: if fillSlot: - raise METALError, ("fill-slot must be within a use-macro", - position) + raise METALError("fill-slot must be within a use-macro", + position) if not self.inMacroUse: if defineMacro: self.pushProgram() @@ -459,13 +583,29 @@ self.inMacroUse = 1 if defineSlot: if not self.inMacroDef: - raise METALError, ( + raise METALError( "define-slot must be within a define-macro", position) self.pushProgram() todo["defineSlot"] = defineSlot - if taldict: + if defineSlot or i18ndict: + + domain = i18ndict.get("domain") or self.i18nContext.domain + source = i18ndict.get("source") or self.i18nContext.source + target = i18ndict.get("target") or self.i18nContext.target + if ( domain != DEFAULT_DOMAIN + or source is not None + or target is not None): + self.i18nContext = TranslationContext(self.i18nContext, + domain=domain, + source=source, + target=target) + self.emit("beginI18nContext", + {"domain": domain, "source": source, + "target": target}) + todo["i18ncontext"] = 1 + if taldict or i18ndict: dict = {} for item in attrlist: key, value = item[:2] @@ -493,18 +633,60 @@ if repeatWhitespace: self.emitText(repeatWhitespace) if content: - todo["content"] = content - if replace: - todo["replace"] = replace + if varname: + todo['i18nvar'] = (varname, I18N_CONTENT, None) + todo["content"] = content + self.pushProgram() + else: + todo["content"] = content + elif replace: + # tal:replace w/ i18n:name has slightly different semantics. What + # we're actually replacing then is the contents of the ${name} + # placeholder. + if varname: + todo['i18nvar'] = (varname, I18N_EXPRESSION, replace) + else: + todo["replace"] = replace self.pushProgram() + # i18n:name w/o tal:replace uses the content as the interpolation + # dictionary values + elif varname: + todo['i18nvar'] = (varname, I18N_REPLACE, None) + self.pushProgram() + if msgid is not None: + self.i18nLevel += 1 + todo['msgid'] = msgid + if i18ndata: + todo['i18ndata'] = i18ndata optTag = omitTag is not None or TALtag if optTag: todo["optional tag"] = omitTag, TALtag self.pushProgram() - if attrsubst: - repldict = parseAttributeReplacements(attrsubst) + if attrsubst or i18nattrs: + if attrsubst: + repldict = TALDefs.parseAttributeReplacements(attrsubst, + self.xml) + else: + repldict = {} + if i18nattrs: + i18nattrs = _parseI18nAttributes(i18nattrs, attrlist, repldict, + self.position, self.xml, + self.source_file) + else: + i18nattrs = {} + # Convert repldict's name-->expr mapping to a + # name-->(compiled_expr, translate) mapping for key, value in repldict.items(): - repldict[key] = self.compileExpression(value) + if i18nattrs.get(key, None): + raise I18NError( + ("attribute [%s] cannot both be part of tal:attributes" + + " and have a msgid in i18n:attributes") % key, + position) + ce = self.compileExpression(value) + repldict[key] = ce, key in i18nattrs, i18nattrs.get(key) + for key in i18nattrs: + if not repldict.has_key(key): + repldict[key] = None, 1, i18nattrs.get(key) else: repldict = {} if replace: @@ -513,7 +695,11 @@ self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend) if optTag: self.pushProgram() - if content: + if content and not varname: + self.pushProgram() + if msgid is not None: + self.pushProgram() + if content and varname: self.pushProgram() if todo and position != (None, None): todo["position"] = position @@ -543,6 +729,10 @@ repldict = todo.get("repldict", {}) scope = todo.get("scope") optTag = todo.get("optional tag") + msgid = todo.get('msgid') + i18ncontext = todo.get("i18ncontext") + varname = todo.get('i18nvar') + i18ndata = todo.get('i18ndata') if implied > 0: if defineMacro or useMacro or defineSlot or fillSlot: @@ -554,14 +744,58 @@ raise exc("%s attributes on <%s> require explicit </%s>" % (what, name, name), position) + # If there's no tal:content or tal:replace in the tag with the + # i18n:name, tal:replace is the default. if content: self.emitSubstitution(content, {}) + # If we're looking at an implicit msgid, emit the insertTranslation + # opcode now, so that the end tag doesn't become part of the implicit + # msgid. If we're looking at an explicit msgid, it's better to emit + # the opcode after the i18nVariable opcode so we can better handle + # tags with both of them in them (and in the latter case, the contents + # would be thrown away for msgid purposes). + # + # Still, we should emit insertTranslation opcode before i18nVariable + # in case tal:content, i18n:translate and i18n:name in the same tag + if msgid is not None: + if (not varname) or ( + varname and (varname[1] == I18N_CONTENT)): + self.emitTranslation(msgid, i18ndata) + self.i18nLevel -= 1 if optTag: self.emitOptTag(name, optTag, isend) elif not isend: + # If we're processing the end tag for a tag that contained + # i18n:name, we need to make sure that optimize() won't collect + # immediately following end tags into the same rawtextOffset, so + # put a spacer here that the optimizer will recognize. + if varname: + self.emit('noop') self.emitEndTag(name) + # If i18n:name appeared in the same tag as tal:replace then we're + # going to do the substitution a little bit differently. The results + # of the expression go into the i18n substitution dictionary. if replace: self.emitSubstitution(replace, repldict) + elif varname: + # o varname[0] is the variable name + # o varname[1] is either + # - I18N_REPLACE for implicit tal:replace + # - I18N_CONTENT for tal:content + # - I18N_EXPRESSION for explicit tal:replace + # o varname[2] will be None for the first two actions and the + # replacement tal expression for the third action. + assert (varname[1] + in [I18N_REPLACE, I18N_CONTENT, I18N_EXPRESSION]) + self.emitI18nVariable(varname) + # Do not test for "msgid is not None", i.e. we only want to test for + # explicit msgids here. See comment above. + if msgid is not None: + # in case tal:content, i18n:translate and i18n:name in the + # same tag insertTranslation opcode has already been + # emitted + if varname and (varname[1] <> I18N_CONTENT): + self.emitTranslation(msgid, i18ndata) if repeat: self.emitRepeat(repeat) if condition: @@ -570,6 +804,10 @@ self.emitOnError(name, onError, optTag and optTag[1], isend) if scope: self.emit("endScope") + if i18ncontext: + self.emit("endI18nContext") + assert self.i18nContext.parent is not None + self.i18nContext = self.i18nContext.parent if defineSlot: self.emitDefineSlot(defineSlot) if fillSlot: @@ -579,6 +817,54 @@ if defineMacro: self.emitDefineMacro(defineMacro) + +def _parseI18nAttributes(i18nattrs, attrlist, repldict, position, + xml, source_file): + + def addAttribute(dic, attr, msgid, position, xml): + if not xml: + attr = attr.lower() + if attr in dic: + raise TALError( + "attribute may only be specified once in i18n:attributes: " + + attr, + position) + dic[attr] = msgid + + d = {} + if ';' in i18nattrs: + i18nattrlist = i18nattrs.split(';') + i18nattrlist = [attr.strip().split() + for attr in i18nattrlist if attr.strip()] + for parts in i18nattrlist: + if len(parts) > 2: + raise TALError("illegal i18n:attributes specification: %r" + % parts, position) + if len(parts) == 2: + attr, msgid = parts + else: + # len(parts) == 1 + attr = parts[0] + msgid = None + addAttribute(d, attr, msgid, position, xml) + else: + i18nattrlist = i18nattrs.split() + if len(i18nattrlist) == 2: + staticattrs = [attr[0] for attr in attrlist if len(attr) == 2] + if (not i18nattrlist[1] in staticattrs) and ( + not i18nattrlist[1] in repldict): + attr, msgid = i18nattrlist + addAttribute(d, attr, msgid, position, xml) + else: + msgid = None + for attr in i18nattrlist: + addAttribute(d, attr, msgid, position, xml) + else: + msgid = None + for attr in i18nattrlist: + addAttribute(d, attr, msgid, position, xml) + return d + def test(): t = TALGenerator() t.pushProgram()
