|  | #!/usr/bin/env python | 
|  | # | 
|  | # | 
|  | # Licensed to the Apache Software Foundation (ASF) under one | 
|  | # or more contributor license agreements.  See the NOTICE file | 
|  | # distributed with this work for additional information | 
|  | # regarding copyright ownership.  The ASF licenses this file | 
|  | # to you under the Apache License, Version 2.0 (the | 
|  | # "License"); you may not use this file except in compliance | 
|  | # with the License.  You may obtain a copy of the License at | 
|  | # | 
|  | #   http://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, | 
|  | # software distributed under the License is distributed on an | 
|  | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | 
|  | # KIND, either express or implied.  See the License for the | 
|  | # specific language governing permissions and limitations | 
|  | # under the License. | 
|  | # | 
|  | # | 
|  | """ezt.py -- easy templating | 
|  |  | 
|  | ezt templates are simply text files in whatever format you so desire | 
|  | (such as XML, HTML, etc.) which contain directives sprinkled | 
|  | throughout.  With these directives it is possible to generate the | 
|  | dynamic content from the ezt templates. | 
|  |  | 
|  | These directives are enclosed in square brackets.  If you are a | 
|  | C-programmer, you might be familar with the #ifdef directives of the C | 
|  | preprocessor 'cpp'.  ezt provides a similar concept.  Additionally EZT | 
|  | has a 'for' directive, which allows it to iterate (repeat) certain | 
|  | subsections of the template according to sequence of data items | 
|  | provided by the application. | 
|  |  | 
|  | The final rendering is performed by the method generate() of the Template | 
|  | class.  Building template instances can either be done using external | 
|  | EZT files (convention: use the suffix .ezt for such files): | 
|  |  | 
|  | >>> template = Template("../templates/log.ezt") | 
|  |  | 
|  | or by calling the parse() method of a template instance directly with | 
|  | a EZT template string: | 
|  |  | 
|  | >>> template = Template() | 
|  | >>> template.parse('''<html><head> | 
|  | ... <title>[title_string]</title></head> | 
|  | ... <body><h1>[title_string]</h1> | 
|  | ...    [for a_sequence] <p>[a_sequence]</p> | 
|  | ...    [end] <hr> | 
|  | ...    The [person] is [if-any state]in[else]out[end]. | 
|  | ... </body> | 
|  | ... </html> | 
|  | ... ''') | 
|  |  | 
|  | The application should build a dictionary 'data' and pass it together | 
|  | with the output fileobject to the templates generate method: | 
|  |  | 
|  | >>> data = {'title_string' : "A Dummy Page", | 
|  | ...         'a_sequence' : ['list item 1', 'list item 2', 'another element'], | 
|  | ...         'person': "doctor", | 
|  | ...         'state' : None } | 
|  | >>> import sys | 
|  | >>> template.generate(sys.stdout, data) | 
|  | <html><head> | 
|  | <title>A Dummy Page</title></head> | 
|  | <body><h1>A Dummy Page</h1> | 
|  | <p>list item 1</p> | 
|  | <p>list item 2</p> | 
|  | <p>another element</p> | 
|  | <hr> | 
|  | The doctor is out. | 
|  | </body> | 
|  | </html> | 
|  |  | 
|  | Template syntax error reporting should be improved.  Currently it is | 
|  | very sparse (template line numbers would be nice): | 
|  |  | 
|  | >>> Template().parse("[if-any where] foo [else] bar [end unexpected args]") | 
|  | Traceback (innermost last): | 
|  | File "<stdin>", line 1, in ? | 
|  | File "ezt.py", line 220, in parse | 
|  | self.program = self._parse(text) | 
|  | File "ezt.py", line 275, in _parse | 
|  | raise ArgCountSyntaxError(str(args[1:])) | 
|  | ArgCountSyntaxError: ['unexpected', 'args'] | 
|  | >>> Template().parse("[if unmatched_end]foo[end]") | 
|  | Traceback (innermost last): | 
|  | File "<stdin>", line 1, in ? | 
|  | File "ezt.py", line 206, in parse | 
|  | self.program = self._parse(text) | 
|  | File "ezt.py", line 266, in _parse | 
|  | raise UnmatchedEndError() | 
|  | UnmatchedEndError | 
|  |  | 
|  |  | 
|  | Directives | 
|  | ========== | 
|  |  | 
|  | Several directives allow the use of dotted qualified names refering to objects | 
|  | or attributes of objects contained in the data dictionary given to the | 
|  | .generate() method. | 
|  |  | 
|  | Qualified names | 
|  | --------------- | 
|  |  | 
|  | Qualified names have two basic forms: a variable reference, or a string | 
|  | constant. References are a name from the data dictionary with optional | 
|  | dotted attributes (where each intermediary is an object with attributes, | 
|  | of course). | 
|  |  | 
|  | Examples: | 
|  |  | 
|  | [varname] | 
|  |  | 
|  | [ob.attr] | 
|  |  | 
|  | ["string"] | 
|  |  | 
|  | Simple directives | 
|  | ----------------- | 
|  |  | 
|  | [QUAL_NAME] | 
|  |  | 
|  | This directive is simply replaced by the value of the qualified name. | 
|  | Numbers are converted to a string, and None becomes an empty string. | 
|  |  | 
|  | [QUAL_NAME QUAL_NAME ...] | 
|  |  | 
|  | The first value defines a substitution format, specifying constant | 
|  | text and indices of the additional arguments. The arguments are then | 
|  | substituted and the resulting is inserted into the output stream. | 
|  |  | 
|  | Example: | 
|  | ["abc %0 def %1 ghi %0" foo bar.baz] | 
|  |  | 
|  | Note that the first value can be any type of qualified name -- a string | 
|  | constant or a variable reference. Use %% to substitute a percent sign. | 
|  | Argument indices are 0-based. | 
|  |  | 
|  | [include "filename"]  or [include QUAL_NAME] | 
|  |  | 
|  | This directive is replaced by content of the named include file. Note | 
|  | that a string constant is more efficient -- the target file is compiled | 
|  | inline. In the variable form, the target file is compiled and executed | 
|  | at runtime. | 
|  |  | 
|  | [insertfile "filename"] or [insertfile QUAL_NAME] | 
|  |  | 
|  | This directive is replace by content from the named file, but as a | 
|  | literal string: directives in the target file are not expanded.  As | 
|  | in the case of the "include" directive, using a string constant for | 
|  | the filename is more efficient than the variable form. | 
|  |  | 
|  | Block directives | 
|  | ---------------- | 
|  |  | 
|  | [for QUAL_NAME] ... [end] | 
|  |  | 
|  | The text within the [for ...] directive and the corresponding [end] | 
|  | is repeated for each element in the sequence referred to by the | 
|  | qualified name in the for directive.  Within the for block this | 
|  | identifiers now refers to the actual item indexed by this loop | 
|  | iteration. | 
|  |  | 
|  | [if-any QUAL_NAME [QUAL_NAME2 ...]] ... [else] ... [end] | 
|  |  | 
|  | Test if any QUAL_NAME value is not None or an empty string or list. | 
|  | The [else] clause is optional.  CAUTION: Numeric values are | 
|  | converted to string, so if QUAL_NAME refers to a numeric value 0, | 
|  | the then-clause is substituted! | 
|  |  | 
|  | [if-index INDEX_FROM_FOR odd] ... [else] ... [end] | 
|  | [if-index INDEX_FROM_FOR even] ... [else] ... [end] | 
|  | [if-index INDEX_FROM_FOR first] ... [else] ... [end] | 
|  | [if-index INDEX_FROM_FOR last] ... [else] ... [end] | 
|  | [if-index INDEX_FROM_FOR NUMBER] ... [else] ... [end] | 
|  |  | 
|  | These five directives work similar to [if-any], but are only useful | 
|  | within a [for ...]-block (see above).  The odd/even directives are | 
|  | for example useful to choose different background colors for | 
|  | adjacent rows in a table.  Similar the first/last directives might | 
|  | be used to remove certain parts (for example "Diff to previous" | 
|  | doesn't make sense, if there is no previous). | 
|  |  | 
|  | [is QUAL_NAME STRING] ... [else] ... [end] | 
|  | [is QUAL_NAME QUAL_NAME] ... [else] ... [end] | 
|  |  | 
|  | The [is ...] directive is similar to the other conditional | 
|  | directives above.  But it allows to compare two value references or | 
|  | a value reference with some constant string. | 
|  |  | 
|  | [define VARIABLE] ... [end] | 
|  |  | 
|  | The [define ...] directive allows you to create and modify template | 
|  | variables from within the template itself.  Essentially, any data | 
|  | between inside the [define ...] and its matching [end] will be | 
|  | expanded using the other template parsing and output generation | 
|  | rules, and then stored as a string value assigned to the variable | 
|  | VARIABLE.  The new (or changed) variable is then available for use | 
|  | with other mechanisms such as [is ...] or [if-any ...], as long as | 
|  | they appear later in the template. | 
|  |  | 
|  | [format "html|xml|js|url|raw"] ... [end] | 
|  |  | 
|  | The [format ...] directive creates a block in which any substitutions | 
|  | are processed as though the template has been instantiated with the | 
|  | the corresponding 'base_format' argument. Comma-separated format | 
|  | specifiers perform nested encodings. In this case the encodings are | 
|  | applied left-to-right.  For example the directive: [format "html,js"] | 
|  | will HTML and then Javascript encode any inserted template variables. | 
|  | """ | 
|  | # | 
|  | # Copyright (C) 2001-2009 Greg Stein. All Rights Reserved. | 
|  | # | 
|  | # Redistribution and use in source and binary forms, with or without | 
|  | # modification, are permitted provided that the following conditions are | 
|  | # met: | 
|  | # | 
|  | # * Redistributions of source code must retain the above copyright | 
|  | #   notice, this list of conditions and the following disclaimer. | 
|  | # | 
|  | # * 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. | 
|  | # | 
|  | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS | 
|  | # IS" AND ANY EXPRESS 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 THE REGENTS OR 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 is maintained by Greg and is available at: | 
|  | #    http://code.google.com/p/ezt/ | 
|  | # | 
|  |  | 
|  | import os, re, sys | 
|  |  | 
|  | if sys.version_info[0] >= 3: | 
|  | # Python >=3.0 | 
|  | long = int | 
|  | unicode = str | 
|  | from io import StringIO | 
|  | from urllib.parse import quote_plus as urllib_parse_quote_plus | 
|  | else: | 
|  | # Python <3.0 | 
|  | from urllib import quote_plus as urllib_parse_quote_plus | 
|  | try: | 
|  | from cStringIO import StringIO | 
|  | except ImportError: | 
|  | from StringIO import StringIO | 
|  |  | 
|  | # | 
|  | # Formatting types | 
|  | # | 
|  | FORMAT_RAW = 'raw' | 
|  | FORMAT_HTML = 'html' | 
|  | FORMAT_XML = 'xml' | 
|  | FORMAT_JS = 'js' | 
|  | FORMAT_URL = 'url' | 
|  |  | 
|  | # | 
|  | # This regular expression matches three alternatives: | 
|  | #   expr: NEWLINE | DIRECTIVE | BRACKET | COMMENT | 
|  | #   DIRECTIVE: '[' ITEM (whitespace ITEM)* '] | 
|  | #   ITEM: STRING | NAME | 
|  | #   STRING: '"' (not-slash-or-dquote | '\' anychar)* '"' | 
|  | #   NAME: (alphanum | '_' | '-' | '.')+ | 
|  | #   BRACKET: '[[]' | 
|  | #   COMMENT: '[#' not-rbracket* ']' | 
|  | # | 
|  | # When used with the split() method, the return value will be composed of | 
|  | # non-matching text and the three paren groups (NEWLINE, DIRECTIVE and | 
|  | # BRACKET). Since the COMMENT matches are not placed into a group, they are | 
|  | # considered a "splitting" value and simply dropped. | 
|  | # | 
|  | _item = r'(?:"(?:[^\\"]|\\.)*"|[-\w.]+)' | 
|  | _re_parse = re.compile(r'(\r?\n)|\[(%s(?: +%s)*)\]|(\[\[\])|\[#[^\]]*\]' % | 
|  | (_item, _item)) | 
|  |  | 
|  | _re_args = re.compile(r'"(?:[^\\"]|\\.)*"|[-\w.]+') | 
|  |  | 
|  | # block commands and their argument counts | 
|  | _block_cmd_specs = { 'if-index':2, 'for':1, 'is':2, 'define':1, 'format':1 } | 
|  | _block_cmds = _block_cmd_specs.keys() | 
|  |  | 
|  | # two regular expressions for compressing whitespace. the first is used to | 
|  | # compress any whitespace including a newline into a single newline. the | 
|  | # second regex is used to compress runs of whitespace into a single space. | 
|  | _re_newline = re.compile('[ \t\r\f\v]*\n\\s*') | 
|  | _re_whitespace = re.compile(r'\s\s+') | 
|  |  | 
|  | # this regex is used to substitute arguments into a value. we split the value, | 
|  | # replace the relevant pieces, and then put it all back together. splitting | 
|  | # will produce a list of: TEXT ( splitter TEXT )*. splitter will be '%' or | 
|  | # an integer. | 
|  | _re_subst = re.compile('%(%|[0-9]+)') | 
|  |  | 
|  | class Template: | 
|  |  | 
|  | def __init__(self, fname=None, compress_whitespace=1, | 
|  | base_format=FORMAT_RAW): | 
|  | self.compress_whitespace = compress_whitespace | 
|  | if fname: | 
|  | self.parse_file(fname, base_format) | 
|  |  | 
|  | def parse_file(self, fname, base_format=FORMAT_RAW): | 
|  | "fname -> a string object with pathname of file containg an EZT template." | 
|  |  | 
|  | self.parse(_FileReader(fname), base_format) | 
|  |  | 
|  | def parse(self, text_or_reader, base_format=FORMAT_RAW): | 
|  | """Parse the template specified by text_or_reader. | 
|  |  | 
|  | The argument should be a string containing the template, or it should | 
|  | specify a subclass of ezt.Reader which can read templates. The base | 
|  | format for printing values is given by base_format. | 
|  | """ | 
|  | if not isinstance(text_or_reader, Reader): | 
|  | # assume the argument is a plain text string | 
|  | text_or_reader = _TextReader(text_or_reader) | 
|  |  | 
|  | self.program = self._parse(text_or_reader, | 
|  | base_printer=_parse_format(base_format)) | 
|  |  | 
|  | def generate(self, fp, data): | 
|  | if hasattr(data, '__getitem__') or hasattr(getattr(data, 'keys', None), '__call__'): | 
|  | # a dictionary-like object was passed. convert it to an | 
|  | # attribute-based object. | 
|  | class _data_ob: | 
|  | def __init__(self, d): | 
|  | vars(self).update(d) | 
|  | data = _data_ob(data) | 
|  |  | 
|  | ctx = _context() | 
|  | ctx.data = data | 
|  | ctx.for_index = { } | 
|  | ctx.defines = { } | 
|  | self._execute(self.program, fp, ctx) | 
|  |  | 
|  | def _parse(self, reader, for_names=None, file_args=(), base_printer=None): | 
|  | """text -> string object containing the template. | 
|  |  | 
|  | This is a private helper function doing the real work for method parse. | 
|  | It returns the parsed template as a 'program'.  This program is a sequence | 
|  | made out of strings or (function, argument) 2-tuples. | 
|  |  | 
|  | Note: comment directives [# ...] are automatically dropped by _re_parse. | 
|  | """ | 
|  |  | 
|  | filename = reader.filename() | 
|  | # parse the template program into: (TEXT NEWLINE DIRECTIVE BRACKET)* TEXT | 
|  | parts = _re_parse.split(reader.text) | 
|  |  | 
|  | program = [ ] | 
|  | stack = [ ] | 
|  | if not for_names: | 
|  | for_names = [ ] | 
|  |  | 
|  | if base_printer is None: | 
|  | base_printer = () | 
|  | printers = [ base_printer ] | 
|  |  | 
|  | one_newline_copied = False | 
|  | line_number = 1 | 
|  | for i in range(len(parts)): | 
|  | piece = parts[i] | 
|  | which = i % 4  # discriminate between: TEXT NEWLINE DIRECTIVE BRACKET | 
|  | if which == 0: | 
|  | # TEXT. append if non-empty. | 
|  | if piece: | 
|  | if self.compress_whitespace: | 
|  | piece = _re_whitespace.sub(' ', piece) | 
|  | program.append(piece) | 
|  | one_newline_copied = False | 
|  | elif which == 1: | 
|  | # NEWLINE. append unless compress_whitespace requested | 
|  | if piece: | 
|  | line_number += 1 | 
|  | if self.compress_whitespace: | 
|  | if not one_newline_copied: | 
|  | program.append('\n') | 
|  | one_newline_copied = True | 
|  | else: | 
|  | program.append(piece) | 
|  | elif which == 3: | 
|  | # BRACKET directive. append '[' if present. | 
|  | if piece: | 
|  | program.append('[') | 
|  | one_newline_copied = False | 
|  | elif piece: | 
|  | # DIRECTIVE is present. | 
|  | one_newline_copied = False | 
|  | args = _re_args.findall(piece) | 
|  | cmd = args[0] | 
|  | if cmd == 'else': | 
|  | if len(args) > 1: | 
|  | raise ArgCountSyntaxError(str(args[1:]), filename, line_number) | 
|  | ### check: don't allow for 'for' cmd | 
|  | idx = stack[-1][1] | 
|  | true_section = program[idx:] | 
|  | del program[idx:] | 
|  | stack[-1][3] = true_section | 
|  | elif cmd == 'end': | 
|  | if len(args) > 1: | 
|  | raise ArgCountSyntaxError(str(args[1:]), filename, line_number) | 
|  | # note: true-section may be None | 
|  | try: | 
|  | cmd, idx, args, true_section, start_line_number = stack.pop() | 
|  | except IndexError: | 
|  | raise UnmatchedEndError(None, filename, line_number) | 
|  | else_section = program[idx:] | 
|  | if cmd == 'format': | 
|  | printers.pop() | 
|  | else: | 
|  | func = getattr(self, '_cmd_' + re.sub('-', '_', cmd)) | 
|  | program[idx:] = [ (func, (args, true_section, else_section), | 
|  | filename, line_number) ] | 
|  | if cmd == 'for': | 
|  | for_names.pop() | 
|  | elif cmd in _block_cmds: | 
|  | if len(args) > _block_cmd_specs[cmd] + 1: | 
|  | raise ArgCountSyntaxError(str(args[1:]), filename, line_number) | 
|  | ### this assumes arg1 is always a ref unless cmd is 'define' | 
|  | if cmd != 'define': | 
|  | args[1] = _prepare_ref(args[1], for_names, file_args) | 
|  |  | 
|  | # handle arg2 for the 'is' command | 
|  | if cmd == 'is': | 
|  | args[2] = _prepare_ref(args[2], for_names, file_args) | 
|  | elif cmd == 'for': | 
|  | for_names.append(args[1][0])  # append the refname | 
|  | elif cmd == 'format': | 
|  | if args[1][0]: | 
|  | raise BadFormatConstantError(str(args[1:]), filename, line_number) | 
|  | printers.append(_parse_format(args[1][1])) | 
|  |  | 
|  | # remember the cmd, current pos, args, and a section placeholder | 
|  | stack.append([cmd, len(program), args[1:], None, line_number]) | 
|  | elif cmd == 'include' or cmd == 'insertfile': | 
|  | is_insertfile = (cmd == 'insertfile') | 
|  | # extra arguments are meaningless when using insertfile | 
|  | if is_insertfile and len(args) != 2: | 
|  | raise ArgCountSyntaxError(str(args), filename, line_number) | 
|  | if args[1][0] == '"': | 
|  | include_filename = args[1][1:-1] | 
|  | if is_insertfile: | 
|  | program.append(reader.read_other(include_filename).text) | 
|  | else: | 
|  | f_args = [ ] | 
|  | for arg in args[2:]: | 
|  | f_args.append(_prepare_ref(arg, for_names, file_args)) | 
|  | program.extend(self._parse(reader.read_other(include_filename), | 
|  | for_names, f_args, printers[-1])) | 
|  | else: | 
|  | if len(args) != 2: | 
|  | raise ArgCountSyntaxError(str(args), filename, line_number) | 
|  | if is_insertfile: | 
|  | cmd = self._cmd_insertfile | 
|  | else: | 
|  | cmd = self._cmd_include | 
|  | program.append((cmd, | 
|  | (_prepare_ref(args[1], for_names, file_args), | 
|  | reader, printers[-1]), filename, line_number)) | 
|  | elif cmd == 'if-any': | 
|  | f_args = [ ] | 
|  | for arg in args[1:]: | 
|  | f_args.append(_prepare_ref(arg, for_names, file_args)) | 
|  | stack.append(['if-any', len(program), f_args, None, line_number]) | 
|  | else: | 
|  | # implied PRINT command | 
|  | if len(args) > 1: | 
|  | f_args = [ ] | 
|  | for arg in args: | 
|  | f_args.append(_prepare_ref(arg, for_names, file_args)) | 
|  | program.append((self._cmd_subst, | 
|  | (printers[-1], f_args[0], f_args[1:]), | 
|  | filename, line_number)) | 
|  | else: | 
|  | valref = _prepare_ref(args[0], for_names, file_args) | 
|  | program.append((self._cmd_print, (printers[-1], valref), | 
|  | filename, line_number)) | 
|  |  | 
|  | if stack: | 
|  | raise UnclosedBlocksError('Block opened at line %s' % stack[-1][4], | 
|  | filename=filename) | 
|  | return program | 
|  |  | 
|  | def _execute(self, program, fp, ctx): | 
|  | """This private helper function takes a 'program' sequence as created | 
|  | by the method '_parse' and executes it step by step.  strings are written | 
|  | to the file object 'fp' and functions are called. | 
|  | """ | 
|  | for step in program: | 
|  | if isinstance(step, str): | 
|  | fp.write(step) | 
|  | else: | 
|  | method, method_args, filename, line_number = step | 
|  | method(method_args, fp, ctx, filename, line_number) | 
|  |  | 
|  | def _cmd_print(self, transforms_valref, fp, ctx, filename, line_number): | 
|  | (transforms, valref) = transforms_valref | 
|  | value = _get_value(valref, ctx, filename, line_number) | 
|  | # if the value has a 'read' attribute, then it is a stream: copy it | 
|  | if hasattr(value, 'read'): | 
|  | while 1: | 
|  | chunk = value.read(16384) | 
|  | if not chunk: | 
|  | break | 
|  | for t in transforms: | 
|  | chunk = t(chunk) | 
|  | fp.write(chunk) | 
|  | else: | 
|  | for t in transforms: | 
|  | value = t(value) | 
|  | fp.write(value) | 
|  |  | 
|  | def _cmd_subst(self, transforms_valref_args, fp, ctx, filename, | 
|  | line_number): | 
|  | (transforms, valref, args) = transforms_valref_args | 
|  | fmt = _get_value(valref, ctx, filename, line_number) | 
|  | parts = _re_subst.split(fmt) | 
|  | for i in range(len(parts)): | 
|  | piece = parts[i] | 
|  | if i%2 == 1 and piece != '%': | 
|  | idx = int(piece) | 
|  | if idx < len(args): | 
|  | piece = _get_value(args[idx], ctx, filename, line_number) | 
|  | else: | 
|  | piece = '<undef>' | 
|  | for t in transforms: | 
|  | piece = t(piece) | 
|  | fp.write(piece) | 
|  |  | 
|  | def _cmd_include(self, valref_reader_printer, fp, ctx, filename, | 
|  | line_number): | 
|  | (valref, reader, printer) = valref_reader_printer | 
|  | fname = _get_value(valref, ctx, filename, line_number) | 
|  | ### note: we don't have the set of for_names to pass into this parse. | 
|  | ### I don't think there is anything to do but document it | 
|  | self._execute(self._parse(reader.read_other(fname), base_printer=printer), | 
|  | fp, ctx) | 
|  |  | 
|  | def _cmd_insertfile(self, valref_reader_printer, fp, ctx, filename, | 
|  | line_number): | 
|  | (valref, reader, printer) = valref_reader_printer | 
|  | fname = _get_value(valref, ctx, filename, line_number) | 
|  | fp.write(reader.read_other(fname).text) | 
|  |  | 
|  | def _cmd_if_any(self, args, fp, ctx, filename, line_number): | 
|  | "If any value is a non-empty string or non-empty list, then T else F." | 
|  | (valrefs, t_section, f_section) = args | 
|  | value = 0 | 
|  | for valref in valrefs: | 
|  | if _get_value(valref, ctx, filename, line_number): | 
|  | value = 1 | 
|  | break | 
|  | self._do_if(value, t_section, f_section, fp, ctx) | 
|  |  | 
|  | def _cmd_if_index(self, args, fp, ctx, filename, line_number): | 
|  | ((valref, value), t_section, f_section) = args | 
|  | list, idx = ctx.for_index[valref[0]] | 
|  | if value == 'even': | 
|  | value = idx % 2 == 0 | 
|  | elif value == 'odd': | 
|  | value = idx % 2 == 1 | 
|  | elif value == 'first': | 
|  | value = idx == 0 | 
|  | elif value == 'last': | 
|  | value = idx == len(list)-1 | 
|  | else: | 
|  | value = idx == int(value) | 
|  | self._do_if(value, t_section, f_section, fp, ctx) | 
|  |  | 
|  | def _cmd_is(self, args, fp, ctx, filename, line_number): | 
|  | ((left_ref, right_ref), t_section, f_section) = args | 
|  | right_value = _get_value(right_ref, ctx, filename, line_number) | 
|  | left_value = _get_value(left_ref, ctx, filename, line_number) | 
|  | value = left_value.lower() == right_value.lower() | 
|  | self._do_if(value, t_section, f_section, fp, ctx) | 
|  |  | 
|  | def _do_if(self, value, t_section, f_section, fp, ctx): | 
|  | if t_section is None: | 
|  | t_section = f_section | 
|  | f_section = None | 
|  | if value: | 
|  | section = t_section | 
|  | else: | 
|  | section = f_section | 
|  | if section is not None: | 
|  | self._execute(section, fp, ctx) | 
|  |  | 
|  | def _cmd_for(self, args, fp, ctx, filename, line_number): | 
|  | ((valref,), unused, section) = args | 
|  | list = _get_value(valref, ctx, filename, line_number) | 
|  | refname = valref[0] | 
|  | if isinstance(list, str): | 
|  | raise NeedSequenceError(refname, filename, line_number) | 
|  | ctx.for_index[refname] = idx = [ list, 0 ] | 
|  | for item in list: | 
|  | self._execute(section, fp, ctx) | 
|  | idx[1] = idx[1] + 1 | 
|  | del ctx.for_index[refname] | 
|  |  | 
|  | def _cmd_define(self, args, fp, ctx, filename, line_number): | 
|  | ((name,), unused, section) = args | 
|  | valfp = StringIO() | 
|  | if section is not None: | 
|  | self._execute(section, valfp, ctx) | 
|  | ctx.defines[name] = valfp.getvalue() | 
|  |  | 
|  | def boolean(value): | 
|  | "Return a value suitable for [if-any bool_var] usage in a template." | 
|  | if value: | 
|  | return 'yes' | 
|  | return None | 
|  |  | 
|  |  | 
|  | def _prepare_ref(refname, for_names, file_args): | 
|  | """refname -> a string containing a dotted identifier. example:"foo.bar.bang" | 
|  | for_names -> a list of active for sequences. | 
|  |  | 
|  | Returns a `value reference', a 3-tuple made out of (refname, start, rest), | 
|  | for fast access later. | 
|  | """ | 
|  | # is the reference a string constant? | 
|  | if refname[0] == '"': | 
|  | return None, refname[1:-1], None | 
|  |  | 
|  | parts = refname.split('.') | 
|  | start = parts[0] | 
|  | rest = parts[1:] | 
|  |  | 
|  | # if this is an include-argument, then just return the prepared ref | 
|  | if start[:3] == 'arg': | 
|  | try: | 
|  | idx = int(start[3:]) | 
|  | except ValueError: | 
|  | pass | 
|  | else: | 
|  | if idx < len(file_args): | 
|  | orig_refname, start, more_rest = file_args[idx] | 
|  | if more_rest is None: | 
|  | # the include-argument was a string constant | 
|  | return None, start, None | 
|  |  | 
|  | # prepend the argument's "rest" for our further processing | 
|  | rest[:0] = more_rest | 
|  |  | 
|  | # rewrite the refname to ensure that any potential 'for' processing | 
|  | # has the correct name | 
|  | ### this can make it hard for debugging include files since we lose | 
|  | ### the 'argNNN' names | 
|  | if not rest: | 
|  | return start, start, [ ] | 
|  | refname = start + '.' + '.'.join(rest) | 
|  |  | 
|  | if for_names: | 
|  | # From last to first part, check if this reference is part of a for loop | 
|  | for i in range(len(parts), 0, -1): | 
|  | name = '.'.join(parts[:i]) | 
|  | if name in for_names: | 
|  | return refname, name, parts[i:] | 
|  |  | 
|  | return refname, start, rest | 
|  |  | 
|  | def _get_value(refname_start_rest, ctx, filename, line_number): | 
|  | """refname_start_rest -> a prepared `value reference' (see above). | 
|  | ctx -> an execution context instance. | 
|  |  | 
|  | Does a name space lookup within the template name space.  Active | 
|  | for blocks take precedence over data dictionary members with the | 
|  | same name. | 
|  | """ | 
|  | (refname, start, rest) = refname_start_rest | 
|  | if rest is None: | 
|  | # it was a string constant | 
|  | return start | 
|  |  | 
|  | # get the starting object | 
|  | if start in ctx.for_index: | 
|  | list, idx = ctx.for_index[start] | 
|  | ob = list[idx] | 
|  | elif start in ctx.defines: | 
|  | ob = ctx.defines[start] | 
|  | elif hasattr(ctx.data, start): | 
|  | ob = getattr(ctx.data, start) | 
|  | else: | 
|  | raise UnknownReference(refname, filename, line_number) | 
|  |  | 
|  | # walk the rest of the dotted reference | 
|  | for attr in rest: | 
|  | try: | 
|  | ob = getattr(ob, attr) | 
|  | except AttributeError: | 
|  | raise UnknownReference(refname, filename, line_number) | 
|  |  | 
|  | # make sure we return a string instead of some various Python types | 
|  | if isinstance(ob, (int, long, float)): | 
|  | return str(ob) | 
|  | if ob is None: | 
|  | return '' | 
|  |  | 
|  | # string or a sequence | 
|  | return ob | 
|  |  | 
|  | def _replace(s, replace_map): | 
|  | for orig, repl in replace_map: | 
|  | s = s.replace(orig, repl) | 
|  | return s | 
|  |  | 
|  | REPLACE_JS_MAP = ( | 
|  | ('\\', r'\\'), ('\t', r'\t'), ('\n', r'\n'), ('\r', r'\r'), | 
|  | ('"', r'\x22'), ('\'', r'\x27'), ('&', r'\x26'), | 
|  | ('<', r'\x3c'), ('>', r'\x3e'), ('=', r'\x3d'), | 
|  | ) | 
|  |  | 
|  | # Various unicode whitespace | 
|  | if sys.version_info[0] >= 3: | 
|  | # Python >=3.0 | 
|  | REPLACE_JS_UNICODE_MAP = ( | 
|  | ('\u0085', r'\u0085'), ('\u2028', r'\u2028'), ('\u2029', r'\u2029') | 
|  | ) | 
|  | else: | 
|  | # Python <3.0 | 
|  | REPLACE_JS_UNICODE_MAP = eval("((u'\u0085', r'\u0085'), (u'\u2028', r'\u2028'), (u'\u2029', r'\u2029'))") | 
|  |  | 
|  | # Why not cgi.escape? It doesn't do single quotes which are occasionally | 
|  | # used to contain HTML attributes and event handler definitions (unfortunately) | 
|  | REPLACE_HTML_MAP = ( | 
|  | ('&', '&'), ('<', '<'), ('>', '>'), | 
|  | ('"', '"'), ('\'', '''), | 
|  | ) | 
|  |  | 
|  | def _js_escape(s): | 
|  | s = _replace(s, REPLACE_JS_MAP) | 
|  | ### perhaps attempt to coerce the string to unicode and then replace? | 
|  | if isinstance(s, unicode): | 
|  | s = _replace(s, REPLACE_JS_UNICODE_MAP) | 
|  | return s | 
|  |  | 
|  | def _html_escape(s): | 
|  | return _replace(s, REPLACE_HTML_MAP) | 
|  |  | 
|  | def _url_escape(s): | 
|  | ### quote_plus barfs on non-ASCII characters. According to | 
|  | ### http://www.w3.org/International/O-URL-code.html URIs should be | 
|  | ### UTF-8 encoded first. | 
|  | if isinstance(s, unicode): | 
|  | s = s.encode('utf8') | 
|  | return urllib_parse_quote_plus(s) | 
|  |  | 
|  | FORMATTERS = { | 
|  | FORMAT_RAW: None, | 
|  | FORMAT_HTML: _html_escape, | 
|  | FORMAT_XML: _html_escape,   ### use the same quoting as HTML for now | 
|  | FORMAT_JS: _js_escape, | 
|  | FORMAT_URL: _url_escape, | 
|  | } | 
|  |  | 
|  | def _parse_format(format_string=FORMAT_RAW): | 
|  | format_funcs = [] | 
|  | try: | 
|  | for fspec in format_string.split(','): | 
|  | format_func = FORMATTERS[fspec] | 
|  | if format_func is not None: | 
|  | format_funcs.append(format_func) | 
|  | except KeyError: | 
|  | raise UnknownFormatConstantError(format_string) | 
|  | return format_funcs | 
|  |  | 
|  | class _context: | 
|  | """A container for the execution context""" | 
|  |  | 
|  |  | 
|  | class Reader: | 
|  | """Abstract class which allows EZT to detect Reader objects.""" | 
|  | def filename(self): | 
|  | return '(%s does not provide filename() method)' % repr(self) | 
|  |  | 
|  | class _FileReader(Reader): | 
|  | """Reads templates from the filesystem.""" | 
|  | def __init__(self, fname): | 
|  | self.text = open(fname, 'rb').read() | 
|  | if sys.version_info[0] >= 3: | 
|  | # Python >=3.0 | 
|  | self.text = self.text.decode() | 
|  | self._dir = os.path.dirname(fname) | 
|  | self.fname = fname | 
|  | def read_other(self, relative): | 
|  | return _FileReader(os.path.join(self._dir, relative)) | 
|  | def filename(self): | 
|  | return self.fname | 
|  |  | 
|  | class _TextReader(Reader): | 
|  | """'Reads' a template from provided text.""" | 
|  | def __init__(self, text): | 
|  | self.text = text | 
|  | def read_other(self, relative): | 
|  | raise BaseUnavailableError() | 
|  | def filename(self): | 
|  | return '(text)' | 
|  |  | 
|  |  | 
|  | class EZTException(Exception): | 
|  | """Parent class of all EZT exceptions.""" | 
|  | def __init__(self, message=None, filename=None, line_number=None): | 
|  | self.message = message | 
|  | self.filename = filename | 
|  | self.line_number = line_number | 
|  | def __str__(self): | 
|  | ret = [] | 
|  | if self.message is not None: | 
|  | ret.append(self.message) | 
|  | if self.filename is not None: | 
|  | ret.append('in file ' + str(self.filename)) | 
|  | if self.line_number is not None: | 
|  | ret.append('at line ' + str(self.line_number)) | 
|  | return ' '.join(ret) | 
|  |  | 
|  | class ArgCountSyntaxError(EZTException): | 
|  | """A bracket directive got the wrong number of arguments.""" | 
|  |  | 
|  | class UnknownReference(EZTException): | 
|  | """The template references an object not contained in the data dictionary.""" | 
|  |  | 
|  | class NeedSequenceError(EZTException): | 
|  | """The object dereferenced by the template is no sequence (tuple or list).""" | 
|  |  | 
|  | class UnclosedBlocksError(EZTException): | 
|  | """This error may be simply a missing [end].""" | 
|  |  | 
|  | class UnmatchedEndError(EZTException): | 
|  | """This error may be caused by a misspelled if directive.""" | 
|  |  | 
|  | class BaseUnavailableError(EZTException): | 
|  | """Base location is unavailable, which disables includes.""" | 
|  |  | 
|  | class BadFormatConstantError(EZTException): | 
|  | """Format specifiers must be string constants.""" | 
|  |  | 
|  | class UnknownFormatConstantError(EZTException): | 
|  | """The format specifier is an unknown value.""" | 
|  |  | 
|  |  | 
|  | # --- standard test environment --- | 
|  | def test_parse(): | 
|  | assert _re_parse.split('[a]') == ['', '[a]', None, ''] | 
|  | assert _re_parse.split('[a] [b]') == \ | 
|  | ['', '[a]', None, ' ', '[b]', None, ''] | 
|  | assert _re_parse.split('[a c] [b]') == \ | 
|  | ['', '[a c]', None, ' ', '[b]', None, ''] | 
|  | assert _re_parse.split('x [a] y [b] z') == \ | 
|  | ['x ', '[a]', None, ' y ', '[b]', None, ' z'] | 
|  | assert _re_parse.split('[a "b" c "d"]') == \ | 
|  | ['', '[a "b" c "d"]', None, ''] | 
|  | assert _re_parse.split(r'["a \"b[foo]" c.d f]') == \ | 
|  | ['', '["a \\"b[foo]" c.d f]', None, ''] | 
|  |  | 
|  | def _test(argv): | 
|  | import doctest, ezt | 
|  | verbose = "-v" in argv | 
|  | return doctest.testmod(ezt, verbose=verbose) | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | # invoke unit test for this module: | 
|  | import sys | 
|  | sys.exit(_test(sys.argv)[0]) |