Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/chameleon/zpt/program.py : 17%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import re
3try:
4 import ast
5except ImportError:
6 from chameleon import ast25 as ast
8try:
9 str = unicode
10except NameError:
11 long = int
13from functools import partial
14from copy import copy
16from ..program import ElementProgram
18from ..namespaces import XML_NS
19from ..namespaces import XMLNS_NS
20from ..namespaces import I18N_NS as I18N
21from ..namespaces import TAL_NS as TAL
22from ..namespaces import METAL_NS as METAL
23from ..namespaces import META_NS as META
25from ..astutil import Static
26from ..astutil import Symbol
27from ..astutil import parse
28from ..astutil import marker
30from .. import tal
31from .. import tales
32from .. import metal
33from .. import i18n
34from .. import nodes
36from ..exc import LanguageError
37from ..exc import ParseError
38from ..exc import CompilationError
40from ..utils import decode_htmlentities
41from ..utils import ImportableMarker
43try:
44 str = unicode
45except NameError:
46 long = int
49missing = object()
51re_trim = re.compile(r'($\s+|\s+^)', re.MULTILINE)
53EMPTY_DICT = Static(ast.Dict(keys=[], values=[]))
54CANCEL_MARKER = ImportableMarker(__name__, "CANCEL")
57def skip(node):
58 return node
61def wrap(node, *wrappers):
62 for wrapper in reversed(wrappers):
63 node = wrapper(node)
64 return node
67def validate_attributes(attributes, namespace, whitelist):
68 for ns, name in attributes:
69 if ns == namespace and name not in whitelist:
70 raise CompilationError(
71 "Bad attribute for namespace '%s'" % ns, name
72 )
75def convert_data_attributes(ns_attrs, attrs, namespaces):
76 d = 0
77 for i, attr in list(enumerate(attrs)):
78 name = attr['name']
79 if name.startswith('data-'):
80 name = name[5:]
81 if '-' not in name:
82 continue
83 prefix, name = name.split('-', 1)
84 ns_attrs[namespaces[prefix], name] = attr['value']
85 attrs.pop(i - d)
86 d += 1
89class MacroProgram(ElementProgram):
90 """Visitor class that generates a program for the ZPT language."""
92 DEFAULT_NAMESPACES = {
93 'xmlns': XMLNS_NS,
94 'xml': XML_NS,
95 'tal': TAL,
96 'metal': METAL,
97 'i18n': I18N,
98 'meta': META,
99 }
101 DROP_NS = TAL, METAL, I18N, META
103 VARIABLE_BLACKLIST = "default", "repeat", "nothing", \
104 "convert", "decode", "translate"
106 _interpolation_enabled = True
107 _whitespace = "\n"
108 _last = ""
109 _cancel_marker = Symbol(CANCEL_MARKER)
111 # Macro name (always trivial for a macro program)
112 name = None
114 # This default marker value has the semantics that if an
115 # expression evaluates to that value, the expression default value
116 # is returned. For an attribute, if there is no default, this
117 # means that the attribute is dropped.
118 default_marker = None
120 # Escape mode (true value means XML-escape)
121 escape = True
123 # Attributes which should have boolean behavior (on true, the
124 # value takes the attribute name, on false, the attribute is
125 # dropped)
126 boolean_attributes = set()
128 # If provided, this should be a set of attributes for implicit
129 # translation. Any attribute whose name is included in the set
130 # will be translated even without explicit markup. Note that all
131 # values should be lowercase strings.
132 implicit_i18n_attributes = set()
134 # If set, text will be translated even without explicit markup.
135 implicit_i18n_translate = False
137 # If set, additional attribute whitespace will be stripped.
138 trim_attribute_space = False
140 # If set, data attributes can be used instead of namespace
141 # attributes, e.g. "data-tal-content" instead of "tal:content".
142 enable_data_attributes = False
144 # If set, XML namespaces are restricted to the list of those
145 # defined and used by the page template language.
146 restricted_namespace = True
148 # If set, expression interpolation is enabled in comments.
149 enable_comment_interpolation = True
151 def __init__(self, *args, **kwargs):
152 # Internal array for switch statements
153 self._switches = []
155 # Internal array for current use macro level
156 self._use_macro = []
158 # Internal array for current interpolation status
159 self._interpolation = [True]
161 # Internal dictionary of macro definitions
162 self._macros = {}
164 # Apply default values from **kwargs to self
165 self._pop_defaults(
166 kwargs,
167 'boolean_attributes',
168 'default_marker',
169 'escape',
170 'implicit_i18n_translate',
171 'implicit_i18n_attributes',
172 'trim_attribute_space',
173 'enable_data_attributes',
174 'enable_comment_interpolation',
175 'restricted_namespace',
176 )
178 super(MacroProgram, self).__init__(*args, **kwargs)
180 @property
181 def macros(self):
182 macros = list(self._macros.items())
183 macros.append((None, nodes.Sequence(self.body)))
185 return tuple(
186 nodes.Macro(name, [nodes.Context(node)])
187 for name, node in macros
188 )
190 def visit_default(self, node):
191 return nodes.Text(node)
193 def visit_element(self, start, end, children):
194 ns = start['ns_attrs']
195 attrs = start['attrs']
197 if self.enable_data_attributes:
198 attrs = list(attrs)
199 convert_data_attributes(ns, attrs, start['ns_map'])
201 for (prefix, attr), encoded in tuple(ns.items()):
202 if prefix == TAL or prefix == METAL:
203 ns[prefix, attr] = decode_htmlentities(encoded)
205 # Validate namespace attributes
206 validate_attributes(ns, TAL, tal.WHITELIST)
207 validate_attributes(ns, METAL, metal.WHITELIST)
208 validate_attributes(ns, I18N, i18n.WHITELIST)
210 # Check attributes for language errors
211 self._check_attributes(start['namespace'], ns)
213 # Remember whitespace for item repetition
214 if self._last is not None:
215 self._whitespace = "\n" + " " * len(self._last.rsplit('\n', 1)[-1])
217 # Set element-local whitespace
218 whitespace = self._whitespace
220 # Set up switch
221 try:
222 clause = ns[TAL, 'switch']
223 except KeyError:
224 switch = None
225 else:
226 value = nodes.Value(clause)
227 switch = value
229 self._switches.append(switch)
231 body = []
233 # Include macro
234 use_macro = ns.get((METAL, 'use-macro'))
235 extend_macro = ns.get((METAL, 'extend-macro'))
236 if use_macro or extend_macro:
237 omit = True
238 slots = []
239 self._use_macro.append(slots)
241 if use_macro:
242 inner = nodes.UseExternalMacro(
243 nodes.Value(use_macro), slots, False
244 )
245 macro_name = use_macro
246 else:
247 inner = nodes.UseExternalMacro(
248 nodes.Value(extend_macro), slots, True
249 )
250 macro_name = extend_macro
252 # While the macro executes, it should have access to the name it was
253 # called with as 'macroname'. Splitting on / mirrors zope.tal and is a
254 # concession to the path expression syntax.
255 macro_name = macro_name.rsplit('/', 1)[-1]
256 inner = nodes.Define(
257 [nodes.Assignment(["macroname"], Static(ast.Str(macro_name)), True)],
258 inner,
259 )
260 STATIC_ATTRIBUTES = None
261 # -or- include tag
262 else:
263 content = nodes.Sequence(body)
265 # tal:content
266 try:
267 clause = ns[TAL, 'content']
268 except KeyError:
269 pass
270 else:
271 key, value = tal.parse_substitution(clause)
272 translate = ns.get((I18N, 'translate')) == ''
273 content = self._make_content_node(
274 value, content, key, translate,
275 )
277 if end is None:
278 # Make sure start-tag has opening suffix.
279 start['suffix'] = ">"
281 # Explicitly set end-tag.
282 end = {
283 'prefix': '</',
284 'name': start['name'],
285 'space': '',
286 'suffix': '>'
287 }
289 # i18n:translate
290 try:
291 clause = ns[I18N, 'translate']
292 except KeyError:
293 pass
294 else:
295 dynamic = ns.get((TAL, 'content')) or ns.get((TAL, 'replace'))
297 if not dynamic:
298 content = nodes.Translate(clause, content)
300 # tal:attributes
301 try:
302 clause = ns[TAL, 'attributes']
303 except KeyError:
304 TAL_ATTRIBUTES = []
305 else:
306 TAL_ATTRIBUTES = tal.parse_attributes(clause)
308 # i18n:attributes
309 try:
310 clause = ns[I18N, 'attributes']
311 except KeyError:
312 I18N_ATTRIBUTES = {}
313 else:
314 I18N_ATTRIBUTES = i18n.parse_attributes(clause)
316 # Prepare attributes from TAL language
317 prepared = tal.prepare_attributes(
318 attrs, TAL_ATTRIBUTES,
319 I18N_ATTRIBUTES, ns, self.DROP_NS
320 )
322 # Create attribute nodes
323 STATIC_ATTRIBUTES = self._create_static_attributes(prepared)
324 ATTRIBUTES = self._create_attributes_nodes(
325 prepared, I18N_ATTRIBUTES, STATIC_ATTRIBUTES,
326 )
328 # Start- and end nodes
329 start_tag = nodes.Start(
330 start['name'],
331 self._maybe_trim(start['prefix']),
332 self._maybe_trim(start['suffix']),
333 ATTRIBUTES
334 )
336 end_tag = nodes.End(
337 end['name'],
338 end['space'],
339 self._maybe_trim(end['prefix']),
340 self._maybe_trim(end['suffix']),
341 ) if end is not None else None
343 # tal:omit-tag
344 try:
345 clause = ns[TAL, 'omit-tag']
346 except KeyError:
347 omit = False
348 else:
349 clause = clause.strip()
351 if clause == "":
352 omit = True
353 else:
354 expression = nodes.Negate(nodes.Value(clause))
355 omit = expression
357 # Wrap start- and end-tags in condition
358 start_tag = nodes.Condition(expression, start_tag)
360 if end_tag is not None:
361 end_tag = nodes.Condition(expression, end_tag)
363 if omit is True or start['namespace'] in self.DROP_NS:
364 inner = content
365 else:
366 inner = nodes.Element(
367 start_tag,
368 end_tag,
369 content,
370 )
372 if omit is not False:
373 inner = nodes.Cache([omit], inner)
375 # tal:replace
376 try:
377 clause = ns[TAL, 'replace']
378 except KeyError:
379 pass
380 else:
381 key, value = tal.parse_substitution(clause)
382 translate = ns.get((I18N, 'translate')) == ''
383 inner = self._make_content_node(
384 value, inner, key, translate
385 )
387 # metal:define-slot
388 try:
389 clause = ns[METAL, 'define-slot']
390 except KeyError:
391 DEFINE_SLOT = skip
392 else:
393 DEFINE_SLOT = partial(nodes.DefineSlot, clause)
395 # tal:define
396 try:
397 clause = ns[TAL, 'define']
398 except KeyError:
399 defines = []
400 else:
401 defines = tal.parse_defines(clause)
403 if defines is None:
404 raise ParseError("Invalid define syntax.", clause)
406 # i18n:target
407 try:
408 target_language = ns[I18N, 'target']
409 except KeyError:
410 pass
411 else:
412 # The name "default" is an alias for the target language
413 # variable. We simply replace it.
414 target_language = target_language.replace(
415 'default', 'target_language'
416 )
418 defines.append(
419 ('local', ("target_language", ), target_language)
420 )
422 assignments = [
423 nodes.Assignment(
424 names, nodes.Value(expr), context == "local")
425 for (context, names, expr) in defines
426 ]
428 # Assign static attributes dictionary to "attrs" value
429 assignments.insert(0, nodes.Alias(["attrs"], STATIC_ATTRIBUTES or EMPTY_DICT))
430 DEFINE = partial(nodes.Define, assignments)
432 # tal:case
433 try:
434 clause = ns[TAL, 'case']
435 except KeyError:
436 CASE = skip
437 else:
438 value = nodes.Value(clause)
439 for switch in reversed(self._switches):
440 if switch is not None:
441 break
442 else:
443 raise LanguageError(
444 "Must define switch on a parent element.", clause
445 )
447 CASE = lambda node: nodes.Define(
448 [nodes.Alias(["default"], self.default_marker)],
449 nodes.Condition(
450 nodes.And([
451 nodes.BinOp(switch, nodes.IsNot, self._cancel_marker),
452 nodes.Or([
453 nodes.BinOp(value, nodes.Equals, switch),
454 nodes.BinOp(value, nodes.Equals, self.default_marker)
455 ])
456 ]),
457 nodes.Cancel([switch], node, self._cancel_marker),
458 ))
460 # tal:repeat
461 try:
462 clause = ns[TAL, 'repeat']
463 except KeyError:
464 REPEAT = skip
465 else:
466 defines = tal.parse_defines(clause)
467 assert len(defines) == 1
468 context, names, expr = defines[0]
470 expression = nodes.Value(expr)
472 if start['namespace'] == TAL:
473 self._last = None
474 self._whitespace = whitespace.lstrip('\n')
475 whitespace = ""
477 REPEAT = partial(
478 nodes.Repeat,
479 names,
480 expression,
481 context == "local",
482 whitespace
483 )
485 # tal:condition
486 try:
487 clause = ns[TAL, 'condition']
488 except KeyError:
489 CONDITION = skip
490 else:
491 expression = nodes.Value(clause)
492 CONDITION = partial(nodes.Condition, expression)
494 # tal:switch
495 if switch is None:
496 SWITCH = skip
497 else:
498 SWITCH = partial(nodes.Cache, [switch])
500 # i18n:domain
501 try:
502 clause = ns[I18N, 'domain']
503 except KeyError:
504 DOMAIN = skip
505 else:
506 DOMAIN = partial(nodes.Domain, clause)
508 # i18n:context
509 try:
510 clause = ns[I18N, 'context']
511 except KeyError:
512 CONTEXT = skip
513 else:
514 CONTEXT = partial(nodes.TxContext, clause)
516 # i18n:name
517 try:
518 clause = ns[I18N, 'name']
519 except KeyError:
520 NAME = skip
521 else:
522 if not clause.strip():
523 NAME = skip
524 else:
525 NAME = partial(nodes.Name, clause)
527 # The "slot" node next is the first node level that can serve
528 # as a macro slot
529 slot = wrap(
530 inner,
531 DEFINE_SLOT,
532 DEFINE,
533 CASE,
534 CONDITION,
535 REPEAT,
536 SWITCH,
537 DOMAIN,
538 CONTEXT,
539 )
541 # metal:fill-slot
542 try:
543 clause = ns[METAL, 'fill-slot']
544 except KeyError:
545 pass
546 else:
547 if not clause.strip():
548 raise LanguageError(
549 "Must provide a non-trivial string for metal:fill-slot.",
550 clause
551 )
553 index = -(1 + int(bool(use_macro or extend_macro)))
555 try:
556 slots = self._use_macro[index]
557 except IndexError:
558 raise LanguageError(
559 "Cannot use metal:fill-slot without metal:use-macro.",
560 clause
561 )
563 slots = self._use_macro[index]
564 slots.append(nodes.FillSlot(clause, slot))
566 # metal:define-macro
567 try:
568 clause = ns[METAL, 'define-macro']
569 except KeyError:
570 pass
571 else:
572 if ns.get((METAL, 'fill-slot')) is not None:
573 raise LanguageError(
574 "Can't have 'fill-slot' and 'define-macro' "
575 "on the same element.",
576 clause
577 )
579 self._macros[clause] = slot
580 slot = nodes.UseInternalMacro(clause)
582 slot = wrap(
583 slot,
584 NAME
585 )
587 # tal:on-error
588 try:
589 clause = ns[TAL, 'on-error']
590 except KeyError:
591 ON_ERROR = skip
592 else:
593 key, value = tal.parse_substitution(clause)
594 translate = ns.get((I18N, 'translate')) == ''
595 fallback = self._make_content_node(
596 value, None, key, translate,
597 )
599 if omit is False and start['namespace'] not in self.DROP_NS:
600 start_tag = copy(start_tag)
602 start_tag.attributes = nodes.Sequence(
603 start_tag.attributes.extract(
604 lambda attribute:
605 isinstance(attribute, nodes.Attribute) and
606 isinstance(attribute.expression, ast.Str)
607 )
608 )
610 if end_tag is None:
611 # Make sure start-tag has opening suffix. We don't
612 # allow self-closing element here.
613 start_tag.suffix = ">"
615 # Explicitly set end-tag.
616 end_tag = nodes.End(start_tag.name, '', '</', '>',)
618 fallback = nodes.Element(
619 start_tag,
620 end_tag,
621 fallback,
622 )
624 ON_ERROR = partial(nodes.OnError, fallback, 'error')
626 clause = ns.get((META, 'interpolation'))
627 if clause in ('false', 'off'):
628 INTERPOLATION = False
629 elif clause in ('true', 'on'):
630 INTERPOLATION = True
631 elif clause is None:
632 INTERPOLATION = self._interpolation[-1]
633 else:
634 raise LanguageError("Bad interpolation setting.", clause)
636 self._interpolation.append(INTERPOLATION)
638 # Visit content body
639 for child in children:
640 body.append(self.visit(*child))
642 self._switches.pop()
643 self._interpolation.pop()
645 if use_macro:
646 self._use_macro.pop()
648 return wrap(
649 slot,
650 ON_ERROR
651 )
653 def visit_start_tag(self, start):
654 return self.visit_element(start, None, [])
656 def visit_cdata(self, node):
657 if not self._interpolation[-1] or not '${' in node:
658 return nodes.Text(node)
660 expr = nodes.Substitution(node, ())
661 return nodes.Interpolation(expr, True, False)
663 def visit_comment(self, node):
664 if node.startswith('<!--!'):
665 return
667 if not self.enable_comment_interpolation:
668 return nodes.Text(node)
670 if node.startswith('<!--?'):
671 return nodes.Text('<!--' + node.lstrip('<!-?'))
673 if not self._interpolation[-1] or not '${' in node:
674 return nodes.Text(node)
676 char_escape = ('&', '<', '>') if self.escape else ()
677 expression = nodes.Substitution(node[4:-3], char_escape)
679 return nodes.Sequence(
680 [nodes.Text(node[:4]),
681 nodes.Interpolation(expression, True, False),
682 nodes.Text(node[-3:])
683 ])
685 def visit_processing_instruction(self, node):
686 if node['name'] != 'python':
687 text = '<?' + node['name'] + node['text'] + '?>'
688 return self.visit_text(text)
690 return nodes.CodeBlock(node['text'])
692 def visit_text(self, node):
693 self._last = node
695 translation = self.implicit_i18n_translate
697 if self._interpolation[-1] and '${' in node:
698 char_escape = ('&', '<', '>') if self.escape else ()
699 expression = nodes.Substitution(node, char_escape)
700 return nodes.Interpolation(expression, True, translation)
702 node = node.replace('$$', '$')
704 if not translation:
705 return nodes.Text(node)
707 match = re.search(r'(\s*)(.*\S)(\s*)', node, flags=re.DOTALL)
708 if match is not None:
709 prefix, text, suffix = match.groups()
710 normalized = re.sub(r'\s+', ' ', text)
711 return nodes.Sequence([
712 nodes.Text(prefix),
713 nodes.Translate(normalized, nodes.Text(normalized), None),
714 nodes.Text(suffix),
715 ])
716 else:
717 return nodes.Text(node)
719 def _pop_defaults(self, kwargs, *attributes):
720 for attribute in attributes:
721 value = kwargs.pop(attribute, None)
722 if value is not None:
723 setattr(self, attribute, value)
725 def _check_attributes(self, namespace, ns):
726 if namespace in self.DROP_NS and ns.get((TAL, 'attributes')):
727 raise LanguageError(
728 "Dynamic attributes not allowed on elements of "
729 "the namespace: %s." % namespace,
730 ns[TAL, 'attributes'],
731 )
733 script = ns.get((TAL, 'script'))
734 if script is not None:
735 raise LanguageError(
736 "The script attribute is unsupported.", script)
738 tal_content = ns.get((TAL, 'content'))
739 if tal_content and ns.get((TAL, 'replace')):
740 raise LanguageError(
741 "You cannot use tal:content and tal:replace at the same time.",
742 tal_content
743 )
745 if tal_content and ns.get((I18N, 'translate')):
746 raise LanguageError(
747 "You cannot use tal:content with non-trivial i18n:translate.",
748 tal_content
749 )
751 def _make_content_node(self, expression, default, key, translate):
752 value = nodes.Value(expression)
753 char_escape = ('&', '<', '>') if key == 'text' else ()
754 content = nodes.Content(value, char_escape, translate)
756 if default is not None:
757 content = nodes.Condition(
758 nodes.BinOp(value, nodes.Is, self.default_marker),
759 default,
760 content,
761 )
763 # Cache expression to avoid duplicate evaluation
764 content = nodes.Cache([value], content)
766 # Define local marker "default"
767 content = nodes.Define(
768 [nodes.Alias(["default"], self.default_marker)],
769 content
770 )
772 return content
774 def _create_attributes_nodes(self, prepared, I18N_ATTRIBUTES, STATIC):
775 attributes = []
777 names = [attr[0] for attr in prepared]
778 filtering = [[]]
780 for i, (name, text, quote, space, eq, expr) in enumerate(prepared):
781 implicit_i18n = (
782 name is not None and
783 name.lower() in self.implicit_i18n_attributes
784 )
786 char_escape = ('&', '<', '>', quote)
787 msgid = I18N_ATTRIBUTES.get(name, missing)
789 # If (by heuristic) ``text`` contains one or more
790 # interpolation expressions, apply interpolation
791 # substitution to the text.
792 if expr is None and text is not None and '${' in text:
793 default = None
794 expr = nodes.Substitution(text, char_escape, default, self.default_marker)
795 translation = implicit_i18n and msgid is missing
796 value = nodes.Interpolation(expr, True, translation)
797 else:
798 default = ast.Str(s=text) if text is not None else None
800 # If the expression is non-trivial, the attribute is
801 # dynamic (computed).
802 if expr is not None:
803 if name is None:
804 expression = nodes.Value(
805 decode_htmlentities(expr),
806 default,
807 self.default_marker
808 )
809 value = nodes.DictAttributes(
810 expression, ('&', '<', '>', '"'), '"',
811 set(filter(None, names[i:]))
812 )
813 for fs in filtering:
814 fs.append(expression)
815 filtering.append([])
816 elif name in self.boolean_attributes:
817 value = nodes.Boolean(expr, name, default, self.default_marker)
818 else:
819 value = nodes.Substitution(
820 decode_htmlentities(expr),
821 char_escape,
822 default,
823 self.default_marker,
824 )
826 # Otherwise, it's a static attribute. We don't include it
827 # here if there's one or more "computed" attributes
828 # (dynamic, from one or more dict values).
829 else:
830 value = ast.Str(s=text)
831 if msgid is missing and implicit_i18n:
832 msgid = text
834 if name is not None:
835 # If translation is required, wrap in a translation
836 # clause
837 if msgid is not missing:
838 value = nodes.Translate(msgid, value)
840 space = self._maybe_trim(space)
842 attribute = nodes.Attribute(
843 name,
844 value,
845 quote,
846 eq,
847 space,
848 default,
849 filtering[-1],
850 )
852 if not isinstance(value, ast.Str):
853 # Always define a ``default`` alias for non-static
854 # expressions.
855 attribute = nodes.Define(
856 [nodes.Alias(["default"], self.default_marker)],
857 attribute,
858 )
860 value = attribute
862 attributes.append(value)
864 result = nodes.Sequence(attributes)
866 # We're caching all expressions found during attribute processing to
867 # enable the filtering functionality which exists to allow later
868 # definitions to override previous ones.
869 expressions = filtering[0]
870 if expressions:
871 return nodes.Cache(expressions, result)
873 return result
875 def _create_static_attributes(self, prepared):
876 static_attrs = {}
878 for name, text, quote, space, eq, expr in prepared:
879 if name is None:
880 continue
882 static_attrs[name] = text if text is not None else expr
884 if not static_attrs:
885 return
887 return Static(parse(repr(static_attrs)).body)
889 def _maybe_trim(self, string):
890 if self.trim_attribute_space:
891 return re_trim.sub(" ", string)
893 return string