Commit | Line | Data |
---|---|---|
c638d827 CR |
1 | ############################################################################## |
2 | # | |
3 | # Copyright (c) 2001, 2002 Zope Corporation and Contributors. | |
4 | # All Rights Reserved. | |
5 | # | |
6 | # This software is subject to the provisions of the Zope Public License, | |
7 | # Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. | |
8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED | |
9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS | |
11 | # FOR A PARTICULAR PURPOSE. | |
12 | # | |
13 | ############################################################################## | |
14 | """ | |
15 | Code generator for TALInterpreter intermediate code. | |
16 | """ | |
17 | ||
18 | import re | |
19 | import cgi | |
20 | ||
21 | import TALDefs | |
22 | ||
23 | from TALDefs import NAME_RE, TAL_VERSION | |
24 | from TALDefs import I18NError, METALError, TALError | |
25 | from TALDefs import parseSubstitution | |
26 | from TranslationContext import TranslationContext, DEFAULT_DOMAIN | |
27 | ||
28 | I18N_REPLACE = 1 | |
29 | I18N_CONTENT = 2 | |
30 | I18N_EXPRESSION = 3 | |
31 | ||
32 | _name_rx = re.compile(NAME_RE) | |
33 | ||
34 | ||
35 | class TALGenerator: | |
36 | ||
37 | inMacroUse = 0 | |
38 | inMacroDef = 0 | |
39 | source_file = None | |
40 | ||
41 | def __init__(self, expressionCompiler=None, xml=1, source_file=None): | |
42 | if not expressionCompiler: | |
43 | from DummyEngine import DummyEngine | |
44 | expressionCompiler = DummyEngine() | |
45 | self.expressionCompiler = expressionCompiler | |
46 | self.CompilerError = expressionCompiler.getCompilerError() | |
47 | # This holds the emitted opcodes representing the input | |
48 | self.program = [] | |
49 | # The program stack for when we need to do some sub-evaluation for an | |
50 | # intermediate result. E.g. in an i18n:name tag for which the | |
51 | # contents describe the ${name} value. | |
52 | self.stack = [] | |
53 | # Another stack of postponed actions. Elements on this stack are a | |
54 | # dictionary; key/values contain useful information that | |
55 | # emitEndElement needs to finish its calculations | |
56 | self.todoStack = [] | |
57 | self.macros = {} | |
58 | self.slots = {} | |
59 | self.slotStack = [] | |
60 | self.xml = xml | |
61 | self.emit("version", TAL_VERSION) | |
62 | self.emit("mode", xml and "xml" or "html") | |
63 | if source_file is not None: | |
64 | self.source_file = source_file | |
65 | self.emit("setSourceFile", source_file) | |
66 | self.i18nContext = TranslationContext() | |
67 | self.i18nLevel = 0 | |
68 | ||
69 | def getCode(self): | |
70 | assert not self.stack | |
71 | assert not self.todoStack | |
72 | return self.optimize(self.program), self.macros | |
73 | ||
74 | def optimize(self, program): | |
75 | output = [] | |
76 | collect = [] | |
77 | cursor = 0 | |
78 | if self.xml: | |
79 | endsep = "/>" | |
80 | else: | |
81 | endsep = " />" | |
82 | for cursor in xrange(len(program)+1): | |
83 | try: | |
84 | item = program[cursor] | |
85 | except IndexError: | |
86 | item = (None, None) | |
87 | opcode = item[0] | |
88 | if opcode == "rawtext": | |
89 | collect.append(item[1]) | |
90 | continue | |
91 | if opcode == "endTag": | |
92 | collect.append("</%s>" % item[1]) | |
93 | continue | |
94 | if opcode == "startTag": | |
95 | if self.optimizeStartTag(collect, item[1], item[2], ">"): | |
96 | continue | |
97 | if opcode == "startEndTag": | |
98 | if self.optimizeStartTag(collect, item[1], item[2], endsep): | |
99 | continue | |
100 | if opcode in ("beginScope", "endScope"): | |
101 | # Push *Scope instructions in front of any text instructions; | |
102 | # this allows text instructions separated only by *Scope | |
103 | # instructions to be joined together. | |
104 | output.append(self.optimizeArgsList(item)) | |
105 | continue | |
106 | if opcode == 'noop': | |
107 | # This is a spacer for end tags in the face of i18n:name | |
108 | # attributes. We can't let the optimizer collect immediately | |
109 | # following end tags into the same rawtextOffset. | |
110 | opcode = None | |
111 | pass | |
112 | text = "".join(collect) | |
113 | if text: | |
114 | i = text.rfind("\n") | |
115 | if i >= 0: | |
116 | i = len(text) - (i + 1) | |
117 | output.append(("rawtextColumn", (text, i))) | |
118 | else: | |
119 | output.append(("rawtextOffset", (text, len(text)))) | |
120 | if opcode != None: | |
121 | output.append(self.optimizeArgsList(item)) | |
122 | collect = [] | |
123 | return self.optimizeCommonTriple(output) | |
124 | ||
125 | def optimizeArgsList(self, item): | |
126 | if len(item) == 2: | |
127 | return item | |
128 | else: | |
129 | return item[0], tuple(item[1:]) | |
130 | ||
131 | # These codes are used to indicate what sort of special actions | |
132 | # are needed for each special attribute. (Simple attributes don't | |
133 | # get action codes.) | |
134 | # | |
135 | # The special actions (which are modal) are handled by | |
136 | # TALInterpreter.attrAction() and .attrAction_tal(). | |
137 | # | |
138 | # Each attribute is represented by a tuple: | |
139 | # | |
140 | # (name, value) -- a simple name/value pair, with | |
141 | # no special processing | |
142 | # | |
143 | # (name, value, action, *extra) -- attribute with special | |
144 | # processing needs, action is a | |
145 | # code that indicates which | |
146 | # branch to take, and *extra | |
147 | # contains additional, | |
148 | # action-specific information | |
149 | # needed by the processing | |
150 | # | |
151 | def optimizeStartTag(self, collect, name, attrlist, end): | |
152 | # return true if the tag can be converted to plain text | |
153 | if not attrlist: | |
154 | collect.append("<%s%s" % (name, end)) | |
155 | return 1 | |
156 | opt = 1 | |
157 | new = ["<" + name] | |
158 | for i in range(len(attrlist)): | |
159 | item = attrlist[i] | |
160 | if len(item) > 2: | |
161 | opt = 0 | |
162 | name, value, action = item[:3] | |
163 | attrlist[i] = (name, value, action) + item[3:] | |
164 | else: | |
165 | if item[1] is None: | |
166 | s = item[0] | |
167 | else: | |
168 | s = '%s="%s"' % (item[0], TALDefs.attrEscape(item[1])) | |
169 | attrlist[i] = item[0], s | |
170 | new.append(" " + s) | |
171 | # if no non-optimizable attributes were found, convert to plain text | |
172 | if opt: | |
173 | new.append(end) | |
174 | collect.extend(new) | |
175 | return opt | |
176 | ||
177 | def optimizeCommonTriple(self, program): | |
178 | if len(program) < 3: | |
179 | return program | |
180 | output = program[:2] | |
181 | prev2, prev1 = output | |
182 | for item in program[2:]: | |
183 | if ( item[0] == "beginScope" | |
184 | and prev1[0] == "setPosition" | |
185 | and prev2[0] == "rawtextColumn"): | |
186 | position = output.pop()[1] | |
187 | text, column = output.pop()[1] | |
188 | prev1 = None, None | |
189 | closeprev = 0 | |
190 | if output and output[-1][0] == "endScope": | |
191 | closeprev = 1 | |
192 | output.pop() | |
193 | item = ("rawtextBeginScope", | |
194 | (text, column, position, closeprev, item[1])) | |
195 | output.append(item) | |
196 | prev2 = prev1 | |
197 | prev1 = item | |
198 | return output | |
199 | ||
200 | def todoPush(self, todo): | |
201 | self.todoStack.append(todo) | |
202 | ||
203 | def todoPop(self): | |
204 | return self.todoStack.pop() | |
205 | ||
206 | def compileExpression(self, expr): | |
207 | try: | |
208 | return self.expressionCompiler.compile(expr) | |
209 | except self.CompilerError, err: | |
210 | raise TALError('%s in expression %s' % (err.args[0], `expr`), | |
211 | self.position) | |
212 | ||
213 | def pushProgram(self): | |
214 | self.stack.append(self.program) | |
215 | self.program = [] | |
216 | ||
217 | def popProgram(self): | |
218 | program = self.program | |
219 | self.program = self.stack.pop() | |
220 | return self.optimize(program) | |
221 | ||
222 | def pushSlots(self): | |
223 | self.slotStack.append(self.slots) | |
224 | self.slots = {} | |
225 | ||
226 | def popSlots(self): | |
227 | slots = self.slots | |
228 | self.slots = self.slotStack.pop() | |
229 | return slots | |
230 | ||
231 | def emit(self, *instruction): | |
232 | self.program.append(instruction) | |
233 | ||
234 | def emitStartTag(self, name, attrlist, isend=0): | |
235 | if isend: | |
236 | opcode = "startEndTag" | |
237 | else: | |
238 | opcode = "startTag" | |
239 | self.emit(opcode, name, attrlist) | |
240 | ||
241 | def emitEndTag(self, name): | |
242 | if self.xml and self.program and self.program[-1][0] == "startTag": | |
243 | # Minimize empty element | |
244 | self.program[-1] = ("startEndTag",) + self.program[-1][1:] | |
245 | else: | |
246 | self.emit("endTag", name) | |
247 | ||
248 | def emitOptTag(self, name, optTag, isend): | |
249 | program = self.popProgram() #block | |
250 | start = self.popProgram() #start tag | |
251 | if (isend or not program) and self.xml: | |
252 | # Minimize empty element | |
253 | start[-1] = ("startEndTag",) + start[-1][1:] | |
254 | isend = 1 | |
255 | cexpr = optTag[0] | |
256 | if cexpr: | |
257 | cexpr = self.compileExpression(optTag[0]) | |
258 | self.emit("optTag", name, cexpr, optTag[1], isend, start, program) | |
259 | ||
260 | def emitRawText(self, text): | |
261 | self.emit("rawtext", text) | |
262 | ||
263 | def emitText(self, text): | |
264 | self.emitRawText(cgi.escape(text)) | |
265 | ||
266 | def emitDefines(self, defines): | |
267 | for part in TALDefs.splitParts(defines): | |
268 | m = re.match( | |
269 | r"(?s)\s*(?:(global|local)\s+)?(%s)\s+(.*)\Z" % NAME_RE, part) | |
270 | if not m: | |
271 | raise TALError("invalid define syntax: " + `part`, | |
272 | self.position) | |
273 | scope, name, expr = m.group(1, 2, 3) | |
274 | scope = scope or "local" | |
275 | cexpr = self.compileExpression(expr) | |
276 | if scope == "local": | |
277 | self.emit("setLocal", name, cexpr) | |
278 | else: | |
279 | self.emit("setGlobal", name, cexpr) | |
280 | ||
281 | def emitOnError(self, name, onError, TALtag, isend): | |
282 | block = self.popProgram() | |
283 | key, expr = parseSubstitution(onError) | |
284 | cexpr = self.compileExpression(expr) | |
285 | if key == "text": | |
286 | self.emit("insertText", cexpr, []) | |
287 | else: | |
288 | assert key == "structure" | |
289 | self.emit("insertStructure", cexpr, {}, []) | |
290 | if TALtag: | |
291 | self.emitOptTag(name, (None, 1), isend) | |
292 | else: | |
293 | self.emitEndTag(name) | |
294 | handler = self.popProgram() | |
295 | self.emit("onError", block, handler) | |
296 | ||
297 | def emitCondition(self, expr): | |
298 | cexpr = self.compileExpression(expr) | |
299 | program = self.popProgram() | |
300 | self.emit("condition", cexpr, program) | |
301 | ||
302 | def emitRepeat(self, arg): | |
303 | m = re.match("(?s)\s*(%s)\s+(.*)\Z" % NAME_RE, arg) | |
304 | if not m: | |
305 | raise TALError("invalid repeat syntax: " + `arg`, | |
306 | self.position) | |
307 | name, expr = m.group(1, 2) | |
308 | cexpr = self.compileExpression(expr) | |
309 | program = self.popProgram() | |
310 | self.emit("loop", name, cexpr, program) | |
311 | ||
312 | def emitSubstitution(self, arg, attrDict={}): | |
313 | key, expr = parseSubstitution(arg) | |
314 | cexpr = self.compileExpression(expr) | |
315 | program = self.popProgram() | |
316 | if key == "text": | |
317 | self.emit("insertText", cexpr, program) | |
318 | else: | |
319 | assert key == "structure" | |
320 | self.emit("insertStructure", cexpr, attrDict, program) | |
321 | ||
322 | def emitI18nVariable(self, stuff): | |
323 | # Used for i18n:name attributes. arg is extra information describing | |
324 | # how the contents of the variable should get filled in, and it will | |
325 | # either be a 1-tuple or a 2-tuple. If arg[0] is None, then the | |
326 | # i18n:name value is taken implicitly from the contents of the tag, | |
327 | # e.g. "I live in <span i18n:name="country">the USA</span>". In this | |
328 | # case, arg[1] is the opcode sub-program describing the contents of | |
329 | # the tag. | |
330 | # | |
331 | # When arg[0] is not None, it contains the tal expression used to | |
332 | # calculate the contents of the variable, e.g. | |
333 | # "I live in <span i18n:name="country" | |
334 | # tal:replace="here/countryOfOrigin" />" | |
335 | varname, action, expression = stuff | |
336 | m = _name_rx.match(varname) | |
337 | if m is None or m.group() != varname: | |
338 | raise TALError("illegal i18n:name: %r" % varname, self.position) | |
339 | key = cexpr = None | |
340 | program = self.popProgram() | |
341 | if action == I18N_REPLACE: | |
342 | # This is a tag with an i18n:name and a tal:replace (implicit or | |
343 | # explicit). Get rid of the first and last elements of the | |
344 | # program, which are the start and end tag opcodes of the tag. | |
345 | program = program[1:-1] | |
346 | elif action == I18N_CONTENT: | |
347 | # This is a tag with an i18n:name and a tal:content | |
348 | # (explicit-only). Keep the first and last elements of the | |
349 | # program, so we keep the start and end tag output. | |
350 | pass | |
351 | else: | |
352 | assert action == I18N_EXPRESSION | |
353 | key, expr = parseSubstitution(expression) | |
354 | cexpr = self.compileExpression(expr) | |
355 | # XXX Would key be anything but 'text' or None? | |
356 | assert key in ('text', None) | |
357 | self.emit('i18nVariable', varname, program, cexpr) | |
358 | ||
359 | def emitTranslation(self, msgid, i18ndata): | |
360 | program = self.popProgram() | |
361 | if i18ndata is None: | |
362 | self.emit('insertTranslation', msgid, program) | |
363 | else: | |
364 | key, expr = parseSubstitution(i18ndata) | |
365 | cexpr = self.compileExpression(expr) | |
366 | assert key == 'text' | |
367 | self.emit('insertTranslation', msgid, program, cexpr) | |
368 | ||
369 | def emitDefineMacro(self, macroName): | |
370 | program = self.popProgram() | |
371 | macroName = macroName.strip() | |
372 | if self.macros.has_key(macroName): | |
373 | raise METALError("duplicate macro definition: %s" % `macroName`, | |
374 | self.position) | |
375 | if not re.match('%s$' % NAME_RE, macroName): | |
376 | raise METALError("invalid macro name: %s" % `macroName`, | |
377 | self.position) | |
378 | self.macros[macroName] = program | |
379 | self.inMacroDef = self.inMacroDef - 1 | |
380 | self.emit("defineMacro", macroName, program) | |
381 | ||
382 | def emitUseMacro(self, expr): | |
383 | cexpr = self.compileExpression(expr) | |
384 | program = self.popProgram() | |
385 | self.inMacroUse = 0 | |
386 | self.emit("useMacro", expr, cexpr, self.popSlots(), program) | |
387 | ||
388 | def emitDefineSlot(self, slotName): | |
389 | program = self.popProgram() | |
390 | slotName = slotName.strip() | |
391 | if not re.match('%s$' % NAME_RE, slotName): | |
392 | raise METALError("invalid slot name: %s" % `slotName`, | |
393 | self.position) | |
394 | self.emit("defineSlot", slotName, program) | |
395 | ||
396 | def emitFillSlot(self, slotName): | |
397 | program = self.popProgram() | |
398 | slotName = slotName.strip() | |
399 | if self.slots.has_key(slotName): | |
400 | raise METALError("duplicate fill-slot name: %s" % `slotName`, | |
401 | self.position) | |
402 | if not re.match('%s$' % NAME_RE, slotName): | |
403 | raise METALError("invalid slot name: %s" % `slotName`, | |
404 | self.position) | |
405 | self.slots[slotName] = program | |
406 | self.inMacroUse = 1 | |
407 | self.emit("fillSlot", slotName, program) | |
408 | ||
409 | def unEmitWhitespace(self): | |
410 | collect = [] | |
411 | i = len(self.program) - 1 | |
412 | while i >= 0: | |
413 | item = self.program[i] | |
414 | if item[0] != "rawtext": | |
415 | break | |
416 | text = item[1] | |
417 | if not re.match(r"\A\s*\Z", text): | |
418 | break | |
419 | collect.append(text) | |
420 | i = i-1 | |
421 | del self.program[i+1:] | |
422 | if i >= 0 and self.program[i][0] == "rawtext": | |
423 | text = self.program[i][1] | |
424 | m = re.search(r"\s+\Z", text) | |
425 | if m: | |
426 | self.program[i] = ("rawtext", text[:m.start()]) | |
427 | collect.append(m.group()) | |
428 | collect.reverse() | |
429 | return "".join(collect) | |
430 | ||
431 | def unEmitNewlineWhitespace(self): | |
432 | collect = [] | |
433 | i = len(self.program) | |
434 | while i > 0: | |
435 | i = i-1 | |
436 | item = self.program[i] | |
437 | if item[0] != "rawtext": | |
438 | break | |
439 | text = item[1] | |
440 | if re.match(r"\A[ \t]*\Z", text): | |
441 | collect.append(text) | |
442 | continue | |
443 | m = re.match(r"(?s)^(.*)(\n[ \t]*)\Z", text) | |
444 | if not m: | |
445 | break | |
446 | text, rest = m.group(1, 2) | |
447 | collect.reverse() | |
448 | rest = rest + "".join(collect) | |
449 | del self.program[i:] | |
450 | if text: | |
451 | self.emit("rawtext", text) | |
452 | return rest | |
453 | return None | |
454 | ||
455 | def replaceAttrs(self, attrlist, repldict): | |
456 | # Each entry in attrlist starts like (name, value). | |
457 | # Result is (name, value, action, expr, xlat) if there is a | |
458 | # tal:attributes entry for that attribute. Additional attrs | |
459 | # defined only by tal:attributes are added here. | |
460 | # | |
461 | # (name, value, action, expr, xlat) | |
462 | if not repldict: | |
463 | return attrlist | |
464 | newlist = [] | |
465 | for item in attrlist: | |
466 | key = item[0] | |
467 | if repldict.has_key(key): | |
468 | expr, xlat, msgid = repldict[key] | |
469 | item = item[:2] + ("replace", expr, xlat, msgid) | |
470 | del repldict[key] | |
471 | newlist.append(item) | |
472 | # Add dynamic-only attributes | |
473 | for key, (expr, xlat, msgid) in repldict.items(): | |
474 | newlist.append((key, None, "insert", expr, xlat, msgid)) | |
475 | return newlist | |
476 | ||
477 | def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict, | |
478 | position=(None, None), isend=0): | |
479 | if not taldict and not metaldict and not i18ndict: | |
480 | # Handle the simple, common case | |
481 | self.emitStartTag(name, attrlist, isend) | |
482 | self.todoPush({}) | |
483 | if isend: | |
484 | self.emitEndElement(name, isend) | |
485 | return | |
486 | ||
487 | self.position = position | |
488 | for key, value in taldict.items(): | |
489 | if key not in TALDefs.KNOWN_TAL_ATTRIBUTES: | |
490 | raise TALError("bad TAL attribute: " + `key`, position) | |
491 | if not (value or key == 'omit-tag'): | |
492 | raise TALError("missing value for TAL attribute: " + | |
493 | `key`, position) | |
494 | for key, value in metaldict.items(): | |
495 | if key not in TALDefs.KNOWN_METAL_ATTRIBUTES: | |
496 | raise METALError("bad METAL attribute: " + `key`, | |
497 | position) | |
498 | if not value: | |
499 | raise TALError("missing value for METAL attribute: " + | |
500 | `key`, position) | |
501 | for key, value in i18ndict.items(): | |
502 | if key not in TALDefs.KNOWN_I18N_ATTRIBUTES: | |
503 | raise I18NError("bad i18n attribute: " + `key`, position) | |
504 | if not value and key in ("attributes", "data", "id"): | |
505 | raise I18NError("missing value for i18n attribute: " + | |
506 | `key`, position) | |
507 | todo = {} | |
508 | defineMacro = metaldict.get("define-macro") | |
509 | useMacro = metaldict.get("use-macro") | |
510 | defineSlot = metaldict.get("define-slot") | |
511 | fillSlot = metaldict.get("fill-slot") | |
512 | define = taldict.get("define") | |
513 | condition = taldict.get("condition") | |
514 | repeat = taldict.get("repeat") | |
515 | content = taldict.get("content") | |
516 | replace = taldict.get("replace") | |
517 | attrsubst = taldict.get("attributes") | |
518 | onError = taldict.get("on-error") | |
519 | omitTag = taldict.get("omit-tag") | |
520 | TALtag = taldict.get("tal tag") | |
521 | i18nattrs = i18ndict.get("attributes") | |
522 | # Preserve empty string if implicit msgids are used. We'll generate | |
523 | # code with the msgid='' and calculate the right implicit msgid during | |
524 | # interpretation phase. | |
525 | msgid = i18ndict.get("translate") | |
526 | varname = i18ndict.get('name') | |
527 | i18ndata = i18ndict.get('data') | |
528 | ||
529 | if varname and not self.i18nLevel: | |
530 | raise I18NError( | |
531 | "i18n:name can only occur inside a translation unit", | |
532 | position) | |
533 | ||
534 | if i18ndata and not msgid: | |
535 | raise I18NError("i18n:data must be accompanied by i18n:translate", | |
536 | position) | |
537 | ||
538 | if len(metaldict) > 1 and (defineMacro or useMacro): | |
539 | raise METALError("define-macro and use-macro cannot be used " | |
540 | "together or with define-slot or fill-slot", | |
541 | position) | |
542 | if replace: | |
543 | if content: | |
544 | raise TALError( | |
545 | "tal:content and tal:replace are mutually exclusive", | |
546 | position) | |
547 | if msgid is not None: | |
548 | raise I18NError( | |
549 | "i18n:translate and tal:replace are mutually exclusive", | |
550 | position) | |
551 | ||
552 | repeatWhitespace = None | |
553 | if repeat: | |
554 | # Hack to include preceding whitespace in the loop program | |
555 | repeatWhitespace = self.unEmitNewlineWhitespace() | |
556 | if position != (None, None): | |
557 | # XXX at some point we should insist on a non-trivial position | |
558 | self.emit("setPosition", position) | |
559 | if self.inMacroUse: | |
560 | if fillSlot: | |
561 | self.pushProgram() | |
562 | if self.source_file is not None: | |
563 | self.emit("setSourceFile", self.source_file) | |
564 | todo["fillSlot"] = fillSlot | |
565 | self.inMacroUse = 0 | |
566 | else: | |
567 | if fillSlot: | |
568 | raise METALError("fill-slot must be within a use-macro", | |
569 | position) | |
570 | if not self.inMacroUse: | |
571 | if defineMacro: | |
572 | self.pushProgram() | |
573 | self.emit("version", TAL_VERSION) | |
574 | self.emit("mode", self.xml and "xml" or "html") | |
575 | if self.source_file is not None: | |
576 | self.emit("setSourceFile", self.source_file) | |
577 | todo["defineMacro"] = defineMacro | |
578 | self.inMacroDef = self.inMacroDef + 1 | |
579 | if useMacro: | |
580 | self.pushSlots() | |
581 | self.pushProgram() | |
582 | todo["useMacro"] = useMacro | |
583 | self.inMacroUse = 1 | |
584 | if defineSlot: | |
585 | if not self.inMacroDef: | |
586 | raise METALError( | |
587 | "define-slot must be within a define-macro", | |
588 | position) | |
589 | self.pushProgram() | |
590 | todo["defineSlot"] = defineSlot | |
591 | ||
592 | if defineSlot or i18ndict: | |
593 | ||
594 | domain = i18ndict.get("domain") or self.i18nContext.domain | |
595 | source = i18ndict.get("source") or self.i18nContext.source | |
596 | target = i18ndict.get("target") or self.i18nContext.target | |
597 | if ( domain != DEFAULT_DOMAIN | |
598 | or source is not None | |
599 | or target is not None): | |
600 | self.i18nContext = TranslationContext(self.i18nContext, | |
601 | domain=domain, | |
602 | source=source, | |
603 | target=target) | |
604 | self.emit("beginI18nContext", | |
605 | {"domain": domain, "source": source, | |
606 | "target": target}) | |
607 | todo["i18ncontext"] = 1 | |
608 | if taldict or i18ndict: | |
609 | dict = {} | |
610 | for item in attrlist: | |
611 | key, value = item[:2] | |
612 | dict[key] = value | |
613 | self.emit("beginScope", dict) | |
614 | todo["scope"] = 1 | |
615 | if onError: | |
616 | self.pushProgram() # handler | |
617 | if TALtag: | |
618 | self.pushProgram() # start | |
619 | self.emitStartTag(name, list(attrlist)) # Must copy attrlist! | |
620 | if TALtag: | |
621 | self.pushProgram() # start | |
622 | self.pushProgram() # block | |
623 | todo["onError"] = onError | |
624 | if define: | |
625 | self.emitDefines(define) | |
626 | todo["define"] = define | |
627 | if condition: | |
628 | self.pushProgram() | |
629 | todo["condition"] = condition | |
630 | if repeat: | |
631 | todo["repeat"] = repeat | |
632 | self.pushProgram() | |
633 | if repeatWhitespace: | |
634 | self.emitText(repeatWhitespace) | |
635 | if content: | |
636 | if varname: | |
637 | todo['i18nvar'] = (varname, I18N_CONTENT, None) | |
638 | todo["content"] = content | |
639 | self.pushProgram() | |
640 | else: | |
641 | todo["content"] = content | |
642 | elif replace: | |
643 | # tal:replace w/ i18n:name has slightly different semantics. What | |
644 | # we're actually replacing then is the contents of the ${name} | |
645 | # placeholder. | |
646 | if varname: | |
647 | todo['i18nvar'] = (varname, I18N_EXPRESSION, replace) | |
648 | else: | |
649 | todo["replace"] = replace | |
650 | self.pushProgram() | |
651 | # i18n:name w/o tal:replace uses the content as the interpolation | |
652 | # dictionary values | |
653 | elif varname: | |
654 | todo['i18nvar'] = (varname, I18N_REPLACE, None) | |
655 | self.pushProgram() | |
656 | if msgid is not None: | |
657 | self.i18nLevel += 1 | |
658 | todo['msgid'] = msgid | |
659 | if i18ndata: | |
660 | todo['i18ndata'] = i18ndata | |
661 | optTag = omitTag is not None or TALtag | |
662 | if optTag: | |
663 | todo["optional tag"] = omitTag, TALtag | |
664 | self.pushProgram() | |
665 | if attrsubst or i18nattrs: | |
666 | if attrsubst: | |
667 | repldict = TALDefs.parseAttributeReplacements(attrsubst, | |
668 | self.xml) | |
669 | else: | |
670 | repldict = {} | |
671 | if i18nattrs: | |
672 | i18nattrs = _parseI18nAttributes(i18nattrs, attrlist, repldict, | |
673 | self.position, self.xml, | |
674 | self.source_file) | |
675 | else: | |
676 | i18nattrs = {} | |
677 | # Convert repldict's name-->expr mapping to a | |
678 | # name-->(compiled_expr, translate) mapping | |
679 | for key, value in repldict.items(): | |
680 | if i18nattrs.get(key, None): | |
681 | raise I18NError( | |
682 | ("attribute [%s] cannot both be part of tal:attributes" + | |
683 | " and have a msgid in i18n:attributes") % key, | |
684 | position) | |
685 | ce = self.compileExpression(value) | |
686 | repldict[key] = ce, key in i18nattrs, i18nattrs.get(key) | |
687 | for key in i18nattrs: | |
688 | if not repldict.has_key(key): | |
689 | repldict[key] = None, 1, i18nattrs.get(key) | |
690 | else: | |
691 | repldict = {} | |
692 | if replace: | |
693 | todo["repldict"] = repldict | |
694 | repldict = {} | |
695 | self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend) | |
696 | if optTag: | |
697 | self.pushProgram() | |
698 | if content and not varname: | |
699 | self.pushProgram() | |
700 | if msgid is not None: | |
701 | self.pushProgram() | |
702 | if content and varname: | |
703 | self.pushProgram() | |
704 | if todo and position != (None, None): | |
705 | todo["position"] = position | |
706 | self.todoPush(todo) | |
707 | if isend: | |
708 | self.emitEndElement(name, isend) | |
709 | ||
710 | def emitEndElement(self, name, isend=0, implied=0): | |
711 | todo = self.todoPop() | |
712 | if not todo: | |
713 | # Shortcut | |
714 | if not isend: | |
715 | self.emitEndTag(name) | |
716 | return | |
717 | ||
718 | self.position = position = todo.get("position", (None, None)) | |
719 | defineMacro = todo.get("defineMacro") | |
720 | useMacro = todo.get("useMacro") | |
721 | defineSlot = todo.get("defineSlot") | |
722 | fillSlot = todo.get("fillSlot") | |
723 | repeat = todo.get("repeat") | |
724 | content = todo.get("content") | |
725 | replace = todo.get("replace") | |
726 | condition = todo.get("condition") | |
727 | onError = todo.get("onError") | |
728 | define = todo.get("define") | |
729 | repldict = todo.get("repldict", {}) | |
730 | scope = todo.get("scope") | |
731 | optTag = todo.get("optional tag") | |
732 | msgid = todo.get('msgid') | |
733 | i18ncontext = todo.get("i18ncontext") | |
734 | varname = todo.get('i18nvar') | |
735 | i18ndata = todo.get('i18ndata') | |
736 | ||
737 | if implied > 0: | |
738 | if defineMacro or useMacro or defineSlot or fillSlot: | |
739 | exc = METALError | |
740 | what = "METAL" | |
741 | else: | |
742 | exc = TALError | |
743 | what = "TAL" | |
744 | raise exc("%s attributes on <%s> require explicit </%s>" % | |
745 | (what, name, name), position) | |
746 | ||
747 | # If there's no tal:content or tal:replace in the tag with the | |
748 | # i18n:name, tal:replace is the default. | |
749 | if content: | |
750 | self.emitSubstitution(content, {}) | |
751 | # If we're looking at an implicit msgid, emit the insertTranslation | |
752 | # opcode now, so that the end tag doesn't become part of the implicit | |
753 | # msgid. If we're looking at an explicit msgid, it's better to emit | |
754 | # the opcode after the i18nVariable opcode so we can better handle | |
755 | # tags with both of them in them (and in the latter case, the contents | |
756 | # would be thrown away for msgid purposes). | |
757 | # | |
758 | # Still, we should emit insertTranslation opcode before i18nVariable | |
759 | # in case tal:content, i18n:translate and i18n:name in the same tag | |
760 | if msgid is not None: | |
761 | if (not varname) or ( | |
762 | varname and (varname[1] == I18N_CONTENT)): | |
763 | self.emitTranslation(msgid, i18ndata) | |
764 | self.i18nLevel -= 1 | |
765 | if optTag: | |
766 | self.emitOptTag(name, optTag, isend) | |
767 | elif not isend: | |
768 | # If we're processing the end tag for a tag that contained | |
769 | # i18n:name, we need to make sure that optimize() won't collect | |
770 | # immediately following end tags into the same rawtextOffset, so | |
771 | # put a spacer here that the optimizer will recognize. | |
772 | if varname: | |
773 | self.emit('noop') | |
774 | self.emitEndTag(name) | |
775 | # If i18n:name appeared in the same tag as tal:replace then we're | |
776 | # going to do the substitution a little bit differently. The results | |
777 | # of the expression go into the i18n substitution dictionary. | |
778 | if replace: | |
779 | self.emitSubstitution(replace, repldict) | |
780 | elif varname: | |
781 | # o varname[0] is the variable name | |
782 | # o varname[1] is either | |
783 | # - I18N_REPLACE for implicit tal:replace | |
784 | # - I18N_CONTENT for tal:content | |
785 | # - I18N_EXPRESSION for explicit tal:replace | |
786 | # o varname[2] will be None for the first two actions and the | |
787 | # replacement tal expression for the third action. | |
788 | assert (varname[1] | |
789 | in [I18N_REPLACE, I18N_CONTENT, I18N_EXPRESSION]) | |
790 | self.emitI18nVariable(varname) | |
791 | # Do not test for "msgid is not None", i.e. we only want to test for | |
792 | # explicit msgids here. See comment above. | |
793 | if msgid is not None: | |
794 | # in case tal:content, i18n:translate and i18n:name in the | |
795 | # same tag insertTranslation opcode has already been | |
796 | # emitted | |
797 | if varname and (varname[1] <> I18N_CONTENT): | |
798 | self.emitTranslation(msgid, i18ndata) | |
799 | if repeat: | |
800 | self.emitRepeat(repeat) | |
801 | if condition: | |
802 | self.emitCondition(condition) | |
803 | if onError: | |
804 | self.emitOnError(name, onError, optTag and optTag[1], isend) | |
805 | if scope: | |
806 | self.emit("endScope") | |
807 | if i18ncontext: | |
808 | self.emit("endI18nContext") | |
809 | assert self.i18nContext.parent is not None | |
810 | self.i18nContext = self.i18nContext.parent | |
811 | if defineSlot: | |
812 | self.emitDefineSlot(defineSlot) | |
813 | if fillSlot: | |
814 | self.emitFillSlot(fillSlot) | |
815 | if useMacro: | |
816 | self.emitUseMacro(useMacro) | |
817 | if defineMacro: | |
818 | self.emitDefineMacro(defineMacro) | |
819 | ||
820 | ||
821 | def _parseI18nAttributes(i18nattrs, attrlist, repldict, position, | |
822 | xml, source_file): | |
823 | ||
824 | def addAttribute(dic, attr, msgid, position, xml): | |
825 | if not xml: | |
826 | attr = attr.lower() | |
827 | if attr in dic: | |
828 | raise TALError( | |
829 | "attribute may only be specified once in i18n:attributes: " | |
830 | + attr, | |
831 | position) | |
832 | dic[attr] = msgid | |
833 | ||
834 | d = {} | |
835 | if ';' in i18nattrs: | |
836 | i18nattrlist = i18nattrs.split(';') | |
837 | i18nattrlist = [attr.strip().split() | |
838 | for attr in i18nattrlist if attr.strip()] | |
839 | for parts in i18nattrlist: | |
840 | if len(parts) > 2: | |
841 | raise TALError("illegal i18n:attributes specification: %r" | |
842 | % parts, position) | |
843 | if len(parts) == 2: | |
844 | attr, msgid = parts | |
845 | else: | |
846 | # len(parts) == 1 | |
847 | attr = parts[0] | |
848 | msgid = None | |
849 | addAttribute(d, attr, msgid, position, xml) | |
850 | else: | |
851 | i18nattrlist = i18nattrs.split() | |
852 | if len(i18nattrlist) == 2: | |
853 | staticattrs = [attr[0] for attr in attrlist if len(attr) == 2] | |
854 | if (not i18nattrlist[1] in staticattrs) and ( | |
855 | not i18nattrlist[1] in repldict): | |
856 | attr, msgid = i18nattrlist | |
857 | addAttribute(d, attr, msgid, position, xml) | |
858 | else: | |
859 | msgid = None | |
860 | for attr in i18nattrlist: | |
861 | addAttribute(d, attr, msgid, position, xml) | |
862 | else: | |
863 | msgid = None | |
864 | for attr in i18nattrlist: | |
865 | addAttribute(d, attr, msgid, position, xml) | |
866 | return d | |
867 | ||
868 | def test(): | |
869 | t = TALGenerator() | |
870 | t.pushProgram() | |
871 | t.emit("bar") | |
872 | p = t.popProgram() | |
873 | t.emit("foo", p) | |
874 | ||
875 | if __name__ == "__main__": | |
876 | test() |