| |
| """ A templating engine for separation of code and HTML. |
| |
| The documentation of this templating engine is separated to two parts: |
| |
| 1. Description of the templating language. |
| |
| 2. Documentation of classes and API of this module that provides |
| a Python implementation of the templating language. |
| |
| All the documentation can be found in 'doc' directory of the |
| distribution tarball or at the homepage of the engine. |
| Latest versions of this module are also available at that website. |
| |
| You can use and redistribute this module under conditions of the |
| GNU General Public License that can be found either at |
| [ http://www.gnu.org/ ] or in file "LICENSE" contained in the |
| distribution tarball of this module. |
| |
| Copyright (c) 2001 Tomas Styblo, tripie@cpan.org |
| |
| @name htmltmpl |
| @version 1.22 |
| @author-name Tomas Styblo |
| @author-email tripie@cpan.org |
| @website http://htmltmpl.sourceforge.net/ |
| @license-name GNU GPL |
| @license-url http://www.gnu.org/licenses/gpl.html |
| """ |
| |
| __version__ = 1.22 |
| __author__ = "Tomas Styblo (tripie@cpan.org)" |
| |
| # All imported modules are part of the standard Python library. |
| |
| from types import * |
| import re |
| import os |
| import os.path |
| import pprint # only for debugging |
| import sys |
| import copy |
| import cgi # for HTML escaping of variables |
| import urllib # for URL escaping of variables |
| import cPickle # for template compilation |
| import gettext |
| |
| INCLUDE_DIR = "inc" |
| |
| # Total number of possible parameters. |
| # Increment if adding a parameter to any statement. |
| PARAMS_NUMBER = 3 |
| |
| # Relative positions of parameters in TemplateCompiler.tokenize(). |
| PARAM_NAME = 1 |
| PARAM_ESCAPE = 2 |
| PARAM_GLOBAL = 3 |
| PARAM_GETTEXT_STRING = 1 |
| |
| # Find a way to lock files. Currently implemented only for UNIX and windows. |
| LOCKTYPE_FCNTL = 1 |
| LOCKTYPE_MSVCRT = 2 |
| LOCKTYPE = None |
| try: |
| import fcntl |
| except: |
| try: |
| import msvcrt |
| except: |
| LOCKTYPE = None |
| else: |
| LOCKTYPE = LOCKTYPE_MSVCRT |
| else: |
| LOCKTYPE = LOCKTYPE_FCNTL |
| LOCK_EX = 1 |
| LOCK_SH = 2 |
| LOCK_UN = 3 |
| |
| ############################################## |
| # CLASS: TemplateManager # |
| ############################################## |
| |
| class TemplateManager: |
| """ Class that manages compilation and precompilation of templates. |
| |
| You should use this class whenever you work with templates |
| that are stored in a file. The class can create a compiled |
| template and transparently manage its precompilation. It also |
| keeps the precompiled templates up-to-date by modification times |
| comparisons. |
| """ |
| |
| def __init__(self, include=1, max_include=5, precompile=1, comments=1, |
| gettext=0, debug=0): |
| """ Constructor. |
| |
| @header |
| __init__(include=1, max_include=5, precompile=1, comments=1, |
| gettext=0, debug=0) |
| |
| @param include Enable or disable included templates. |
| This optional parameter can be used to enable or disable |
| <em>TMPL_INCLUDE</em> inclusion of templates. Disabling of |
| inclusion can improve performance a bit. The inclusion is |
| enabled by default. |
| |
| @param max_include Maximum depth of nested inclusions. |
| This optional parameter can be used to specify maximum depth of |
| nested <em>TMPL_INCLUDE</em> inclusions. It defaults to 5. |
| This setting prevents infinite recursive inclusions. |
| |
| @param precompile Enable or disable precompilation of templates. |
| This optional parameter can be used to enable or disable |
| creation and usage of precompiled templates. |
| |
| A precompiled template is saved to the same directory in |
| which the main template file is located. You need write |
| permissions to that directory. |
| |
| Precompilation provides a significant performance boost because |
| it's not necessary to parse the templates over and over again. |
| The boost is especially noticeable when templates that include |
| other templates are used. |
| |
| Comparison of modification times of the main template and all |
| included templates is used to ensure that the precompiled |
| templates are up-to-date. Templates are also recompiled if the |
| htmltmpl module is updated. |
| |
| The <em>TemplateError</em>exception is raised when the precompiled |
| template cannot be saved. Precompilation is enabled by default. |
| |
| Precompilation is available only on UNIX and Windows platforms, |
| because proper file locking which is necessary to ensure |
| multitask safe behaviour is platform specific and is not |
| implemented for other platforms. Attempts to enable precompilation |
| on the other platforms result in raise of the |
| <em>TemplateError</em> exception. |
| |
| @param comments Enable or disable template comments. |
| This optional parameter can be used to enable or disable |
| template comments. |
| Disabling of the comments can improve performance a bit. |
| Comments are enabled by default. |
| |
| @param gettext Enable or disable gettext support. |
| |
| @param debug Enable or disable debugging messages. |
| This optional parameter is a flag that can be used to enable |
| or disable debugging messages which are printed to the standard |
| error output. The debugging messages are disabled by default. |
| """ |
| # Save the optional parameters. |
| # These values are not modified by any method. |
| self._include = include |
| self._max_include = max_include |
| self._precompile = precompile |
| self._comments = comments |
| self._gettext = gettext |
| self._debug = debug |
| |
| # Find what module to use to lock files. |
| # File locking is necessary for the 'precompile' feature to be |
| # multitask/thread safe. Currently it works only on UNIX |
| # and Windows. Anyone willing to implement it on Mac ? |
| if precompile and not LOCKTYPE: |
| raise TemplateError, "Template precompilation is not "\ |
| "available on this platform." |
| self.DEB("INIT DONE") |
| |
| def prepare(self, file): |
| """ Preprocess, parse, tokenize and compile the template. |
| |
| If precompilation is enabled then this method tries to load |
| a precompiled form of the template from the same directory |
| in which the template source file is located. If it succeeds, |
| then it compares modification times stored in the precompiled |
| form to modification times of source files of the template, |
| including source files of all templates included via the |
| <em>TMPL_INCLUDE</em> statements. If any of the modification times |
| differs, then the template is recompiled and the precompiled |
| form updated. |
| |
| If precompilation is disabled, then this method parses and |
| compiles the template. |
| |
| @header prepare(file) |
| |
| @return Compiled template. |
| The methods returns an instance of the <em>Template</em> class |
| which is a compiled form of the template. This instance can be |
| used as input for the <em>TemplateProcessor</em>. |
| |
| @param file Path to the template file to prepare. |
| The method looks for the template file in current directory |
| if the parameter is a relative path. All included templates must |
| be placed in subdirectory <strong>'inc'</strong> of the |
| directory in which the main template file is located. |
| """ |
| compiled = None |
| if self._precompile: |
| if self.is_precompiled(file): |
| try: |
| precompiled = self.load_precompiled(file) |
| except PrecompiledError, template: |
| print >> sys.stderr, "Htmltmpl: bad precompiled "\ |
| "template '%s' removed" % template |
| compiled = self.compile(file) |
| self.save_precompiled(compiled) |
| else: |
| precompiled.debug(self._debug) |
| compile_params = (self._include, self._max_include, |
| self._comments, self._gettext) |
| if precompiled.is_uptodate(compile_params): |
| self.DEB("PRECOMPILED: UPTODATE") |
| compiled = precompiled |
| else: |
| self.DEB("PRECOMPILED: NOT UPTODATE") |
| compiled = self.update(precompiled) |
| else: |
| self.DEB("PRECOMPILED: NOT PRECOMPILED") |
| compiled = self.compile(file) |
| self.save_precompiled(compiled) |
| else: |
| self.DEB("PRECOMPILATION DISABLED") |
| compiled = self.compile(file) |
| return compiled |
| |
| def update(self, template): |
| """ Update (recompile) a compiled template. |
| |
| This method recompiles a template compiled from a file. |
| If precompilation is enabled then the precompiled form saved on |
| disk is also updated. |
| |
| @header update(template) |
| |
| @return Recompiled template. |
| It's ensured that the returned template is up-to-date. |
| |
| @param template A compiled template. |
| This parameter should be an instance of the <em>Template</em> |
| class, created either by the <em>TemplateManager</em> or by the |
| <em>TemplateCompiler</em>. The instance must represent a template |
| compiled from a file on disk. |
| """ |
| self.DEB("UPDATE") |
| updated = self.compile(template.file()) |
| if self._precompile: |
| self.save_precompiled(updated) |
| return updated |
| |
| ############################################## |
| # PRIVATE METHODS # |
| ############################################## |
| |
| def DEB(self, str): |
| """ Print debugging message to stderr if debugging is enabled. |
| @hidden |
| """ |
| if self._debug: print >> sys.stderr, str |
| |
| def lock_file(self, file, lock): |
| """ Provide platform independent file locking. |
| @hidden |
| """ |
| fd = file.fileno() |
| if LOCKTYPE == LOCKTYPE_FCNTL: |
| if lock == LOCK_SH: |
| fcntl.flock(fd, fcntl.LOCK_SH) |
| elif lock == LOCK_EX: |
| fcntl.flock(fd, fcntl.LOCK_EX) |
| elif lock == LOCK_UN: |
| fcntl.flock(fd, fcntl.LOCK_UN) |
| else: |
| raise TemplateError, "BUG: bad lock in lock_file" |
| elif LOCKTYPE == LOCKTYPE_MSVCRT: |
| if lock == LOCK_SH: |
| # msvcrt does not support shared locks :-( |
| msvcrt.locking(fd, msvcrt.LK_LOCK, 1) |
| elif lock == LOCK_EX: |
| msvcrt.locking(fd, msvcrt.LK_LOCK, 1) |
| elif lock == LOCK_UN: |
| msvcrt.locking(fd, msvcrt.LK_UNLCK, 1) |
| else: |
| raise TemplateError, "BUG: bad lock in lock_file" |
| else: |
| raise TemplateError, "BUG: bad locktype in lock_file" |
| |
| def compile(self, file): |
| """ Compile the template. |
| @hidden |
| """ |
| return TemplateCompiler(self._include, self._max_include, |
| self._comments, self._gettext, |
| self._debug).compile(file) |
| |
| def is_precompiled(self, file): |
| """ Return true if the template is already precompiled on the disk. |
| This method doesn't check whether the compiled template is |
| uptodate. |
| @hidden |
| """ |
| filename = file + "c" # "template.tmplc" |
| if os.path.isfile(filename): |
| return 1 |
| else: |
| return 0 |
| |
| def load_precompiled(self, file): |
| """ Load precompiled template from disk. |
| |
| Remove the precompiled template file and recompile it |
| if the file contains corrupted or unpicklable data. |
| |
| @hidden |
| """ |
| filename = file + "c" # "template.tmplc" |
| self.DEB("LOADING PRECOMPILED") |
| try: |
| remove_bad = 0 |
| file = None |
| try: |
| file = open(filename, "rb") |
| self.lock_file(file, LOCK_SH) |
| precompiled = cPickle.load(file) |
| except IOError, (errno, errstr): |
| raise TemplateError, "IO error in load precompiled "\ |
| "template '%s': (%d) %s"\ |
| % (filename, errno, errstr) |
| except cPickle.UnpicklingError: |
| remove_bad = 1 |
| raise PrecompiledError, filename |
| except: |
| remove_bad = 1 |
| raise |
| else: |
| return precompiled |
| finally: |
| if file: |
| self.lock_file(file, LOCK_UN) |
| file.close() |
| if remove_bad and os.path.isfile(filename): |
| # X: We may lose the original exception here, raising OSError. |
| os.remove(filename) |
| |
| def save_precompiled(self, template): |
| """ Save compiled template to disk in precompiled form. |
| |
| Associated metadata is also saved. It includes: filename of the |
| main template file, modification time of the main template file, |
| modification times of all included templates and version of the |
| htmltmpl module which compiled the template. |
| |
| The method removes a file which is saved only partially because |
| of some error. |
| |
| @hidden |
| """ |
| filename = template.file() + "c" # creates "template.tmplc" |
| # Check if we have write permission to the template's directory. |
| template_dir = os.path.dirname(os.path.abspath(filename)) |
| if not os.access(template_dir, os.W_OK): |
| raise TemplateError, "Cannot save precompiled templates "\ |
| "to '%s': write permission denied."\ |
| % template_dir |
| try: |
| remove_bad = 0 |
| file = None |
| try: |
| file = open(filename, "wb") # may truncate existing file |
| self.lock_file(file, LOCK_EX) |
| BINARY = 1 |
| READABLE = 0 |
| if self._debug: |
| cPickle.dump(template, file, READABLE) |
| else: |
| cPickle.dump(template, file, BINARY) |
| except IOError, (errno, errstr): |
| remove_bad = 1 |
| raise TemplateError, "IO error while saving precompiled "\ |
| "template '%s': (%d) %s"\ |
| % (filename, errno, errstr) |
| except cPickle.PicklingError, error: |
| remove_bad = 1 |
| raise TemplateError, "Pickling error while saving "\ |
| "precompiled template '%s': %s"\ |
| % (filename, error) |
| except: |
| remove_bad = 1 |
| raise |
| else: |
| self.DEB("SAVING PRECOMPILED") |
| finally: |
| if file: |
| self.lock_file(file, LOCK_UN) |
| file.close() |
| if remove_bad and os.path.isfile(filename): |
| # X: We may lose the original exception here, raising OSError. |
| os.remove(filename) |
| |
| |
| ############################################## |
| # CLASS: TemplateProcessor # |
| ############################################## |
| |
| class TemplateProcessor: |
| """ Fill the template with data and process it. |
| |
| This class provides actual processing of a compiled template. |
| Use it to set template variables and loops and then obtain |
| result of the processing. |
| """ |
| |
| def __init__(self, html_escape=1, magic_vars=1, global_vars=0, debug=0): |
| """ Constructor. |
| |
| @header __init__(html_escape=1, magic_vars=1, global_vars=0, |
| debug=0) |
| |
| @param html_escape Enable or disable HTML escaping of variables. |
| This optional parameter is a flag that can be used to enable or |
| disable automatic HTML escaping of variables. |
| All variables are by default automatically HTML escaped. |
| The escaping process substitutes HTML brackets, ampersands and |
| double quotes with appropriate HTML entities. |
| |
| @param magic_vars Enable or disable loop magic variables. |
| This parameter can be used to enable or disable |
| "magic" context variables, that are automatically defined inside |
| loops. Magic variables are enabled by default. |
| |
| Refer to the language specification for description of these |
| magic variables. |
| |
| @param global_vars Globally activate global lookup of variables. |
| This optional parameter is a flag that can be used to specify |
| whether variables which cannot be found in the current scope |
| should be automatically looked up in enclosing scopes. |
| |
| Automatic global lookup is disabled by default. Global lookup |
| can be overriden on a per-variable basis by the |
| <strong>GLOBAL</strong> parameter of a <strong>TMPL_VAR</strong> |
| statement. |
| |
| @param debug Enable or disable debugging messages. |
| """ |
| self._html_escape = html_escape |
| self._magic_vars = magic_vars |
| self._global_vars = global_vars |
| self._debug = debug |
| |
| # Data structure containing variables and loops set by the |
| # application. Use debug=1, process some template and |
| # then check stderr to see how the structure looks. |
| # It's modified only by set() and reset() methods. |
| self._vars = {} |
| |
| # Following variables are for multipart templates. |
| self._current_part = 1 |
| self._current_pos = 0 |
| |
| def set(self, var, value): |
| """ Associate a value with top-level template variable or loop. |
| |
| A template identifier can represent either an ordinary variable |
| (string) or a loop. |
| |
| To assign a value to a string identifier pass a scalar |
| as the 'value' parameter. This scalar will be automatically |
| converted to string. |
| |
| To assign a value to a loop identifier pass a list of mappings as |
| the 'value' parameter. The engine iterates over this list and |
| assigns values from the mappings to variables in a template loop |
| block if a key in the mapping corresponds to a name of a variable |
| in the loop block. The number of mappings contained in this list |
| is equal to number of times the loop block is repeated in the |
| output. |
| |
| @header set(var, value) |
| @return No return value. |
| |
| @param var Name of template variable or loop. |
| @param value The value to associate. |
| |
| """ |
| # The correctness of character case is verified only for top-level |
| # variables. |
| if self.is_ordinary_var(value): |
| # template top-level ordinary variable |
| if not var.islower(): |
| raise TemplateError, "Invalid variable name '%s'." % var |
| elif type(value) == ListType: |
| # template top-level loop |
| if var != var.capitalize(): |
| raise TemplateError, "Invalid loop name '%s'." % var |
| else: |
| raise TemplateError, "Value of toplevel variable '%s' must "\ |
| "be either a scalar or a list." % var |
| self._vars[var] = value |
| self.DEB("VALUE SET: " + str(var)) |
| |
| def reset(self, keep_data=0): |
| """ Reset the template data. |
| |
| This method resets the data contained in the template processor |
| instance. The template processor instance can be used to process |
| any number of templates, but this method must be called after |
| a template is processed to reuse the instance, |
| |
| @header reset(keep_data=0) |
| @return No return value. |
| |
| @param keep_data Do not reset the template data. |
| Use this flag if you do not want the template data to be erased. |
| This way you can reuse the data contained in the instance of |
| the <em>TemplateProcessor</em>. |
| """ |
| self._current_part = 1 |
| self._current_pos = 0 |
| if not keep_data: |
| self._vars.clear() |
| self.DEB("RESET") |
| |
| def process(self, template, part=None): |
| """ Process a compiled template. Return the result as string. |
| |
| This method actually processes a template and returns |
| the result. |
| |
| @header process(template, part=None) |
| @return Result of the processing as string. |
| |
| @param template A compiled template. |
| Value of this parameter must be an instance of the |
| <em>Template</em> class created either by the |
| <em>TemplateManager</em> or by the <em>TemplateCompiler</em>. |
| |
| @param part The part of a multipart template to process. |
| This parameter can be used only together with a multipart |
| template. It specifies the number of the part to process. |
| It must be greater than zero, because the parts are numbered |
| from one. |
| |
| The parts must be processed in the right order. You |
| cannot process a part which precedes an already processed part. |
| |
| If this parameter is not specified, then the whole template |
| is processed, or all remaining parts are processed. |
| """ |
| self.DEB("APP INPUT:") |
| if self._debug: pprint.pprint(self._vars, sys.stderr) |
| if part != None and (part == 0 or part < self._current_part): |
| raise TemplateError, "process() - invalid part number" |
| |
| # This flag means "jump behind the end of current statement" or |
| # "skip the parameters of current statement". |
| # Even parameters that actually are not present in the template |
| # do appear in the list of tokens as empty items ! |
| skip_params = 0 |
| |
| # Stack for enabling or disabling output in response to TMPL_IF, |
| # TMPL_UNLESS, TMPL_ELSE and TMPL_LOOPs with no passes. |
| output_control = [] |
| ENABLE_OUTPUT = 1 |
| DISABLE_OUTPUT = 0 |
| |
| # Stacks for data related to loops. |
| loop_name = [] # name of a loop |
| loop_pass = [] # current pass of a loop (counted from zero) |
| loop_start = [] # index of loop start in token list |
| loop_total = [] # total number of passes in a loop |
| |
| tokens = template.tokens() |
| len_tokens = len(tokens) |
| out = "" # buffer for processed output |
| |
| # Recover position at which we ended after processing of last part. |
| i = self._current_pos |
| |
| # Process the list of tokens. |
| while 1: |
| if i == len_tokens: break |
| if skip_params: |
| # Skip the parameters following a statement. |
| skip_params = 0 |
| i += PARAMS_NUMBER |
| continue |
| |
| token = tokens[i] |
| if token.startswith("<TMPL_") or \ |
| token.startswith("</TMPL_"): |
| if token == "<TMPL_VAR": |
| # TMPL_VARs should be first. They are the most common. |
| var = tokens[i + PARAM_NAME] |
| if not var: |
| raise TemplateError, "No identifier in <TMPL_VAR>." |
| escape = tokens[i + PARAM_ESCAPE] |
| globalp = tokens[i + PARAM_GLOBAL] |
| skip_params = 1 |
| |
| # If output of current block is not disabled then append |
| # the substitued and escaped variable to the output. |
| if DISABLE_OUTPUT not in output_control: |
| value = str(self.find_value(var, loop_name, loop_pass, |
| loop_total, globalp)) |
| out += self.escape(value, escape) |
| self.DEB("VAR: " + str(var)) |
| |
| elif token == "<TMPL_LOOP": |
| var = tokens[i + PARAM_NAME] |
| if not var: |
| raise TemplateError, "No identifier in <TMPL_LOOP>." |
| skip_params = 1 |
| |
| # Find total number of passes in this loop. |
| passtotal = self.find_value(var, loop_name, loop_pass, |
| loop_total) |
| if not passtotal: passtotal = 0 |
| # Push data for this loop on the stack. |
| loop_total.append(passtotal) |
| loop_start.append(i) |
| loop_pass.append(0) |
| loop_name.append(var) |
| |
| # Disable output of loop block if the number of passes |
| # in this loop is zero. |
| if passtotal == 0: |
| # This loop is empty. |
| output_control.append(DISABLE_OUTPUT) |
| self.DEB("LOOP: DISABLE: " + str(var)) |
| else: |
| output_control.append(ENABLE_OUTPUT) |
| self.DEB("LOOP: FIRST PASS: %s TOTAL: %d"\ |
| % (var, passtotal)) |
| |
| elif token == "<TMPL_IF": |
| var = tokens[i + PARAM_NAME] |
| if not var: |
| raise TemplateError, "No identifier in <TMPL_IF>." |
| globalp = tokens[i + PARAM_GLOBAL] |
| skip_params = 1 |
| if self.find_value(var, loop_name, loop_pass, |
| loop_total, globalp): |
| output_control.append(ENABLE_OUTPUT) |
| self.DEB("IF: ENABLE: " + str(var)) |
| else: |
| output_control.append(DISABLE_OUTPUT) |
| self.DEB("IF: DISABLE: " + str(var)) |
| |
| elif token == "<TMPL_UNLESS": |
| var = tokens[i + PARAM_NAME] |
| if not var: |
| raise TemplateError, "No identifier in <TMPL_UNLESS>." |
| globalp = tokens[i + PARAM_GLOBAL] |
| skip_params = 1 |
| if self.find_value(var, loop_name, loop_pass, |
| loop_total, globalp): |
| output_control.append(DISABLE_OUTPUT) |
| self.DEB("UNLESS: DISABLE: " + str(var)) |
| else: |
| output_control.append(ENABLE_OUTPUT) |
| self.DEB("UNLESS: ENABLE: " + str(var)) |
| |
| elif token == "</TMPL_LOOP": |
| skip_params = 1 |
| if not loop_name: |
| raise TemplateError, "Unmatched </TMPL_LOOP>." |
| |
| # If this loop was not disabled, then record the pass. |
| if loop_total[-1] > 0: loop_pass[-1] += 1 |
| |
| if loop_pass[-1] == loop_total[-1]: |
| # There are no more passes in this loop. Pop |
| # the loop from stack. |
| loop_pass.pop() |
| loop_name.pop() |
| loop_start.pop() |
| loop_total.pop() |
| output_control.pop() |
| self.DEB("LOOP: END") |
| else: |
| # Jump to the beggining of this loop block |
| # to process next pass of the loop. |
| i = loop_start[-1] |
| self.DEB("LOOP: NEXT PASS") |
| |
| elif token == "</TMPL_IF": |
| skip_params = 1 |
| if not output_control: |
| raise TemplateError, "Unmatched </TMPL_IF>." |
| output_control.pop() |
| self.DEB("IF: END") |
| |
| elif token == "</TMPL_UNLESS": |
| skip_params = 1 |
| if not output_control: |
| raise TemplateError, "Unmatched </TMPL_UNLESS>." |
| output_control.pop() |
| self.DEB("UNLESS: END") |
| |
| elif token == "<TMPL_ELSE": |
| skip_params = 1 |
| if not output_control: |
| raise TemplateError, "Unmatched <TMPL_ELSE>." |
| if output_control[-1] == DISABLE_OUTPUT: |
| # Condition was false, activate the ELSE block. |
| output_control[-1] = ENABLE_OUTPUT |
| self.DEB("ELSE: ENABLE") |
| elif output_control[-1] == ENABLE_OUTPUT: |
| # Condition was true, deactivate the ELSE block. |
| output_control[-1] = DISABLE_OUTPUT |
| self.DEB("ELSE: DISABLE") |
| else: |
| raise TemplateError, "BUG: ELSE: INVALID FLAG" |
| |
| elif token == "<TMPL_BOUNDARY": |
| if part and part == self._current_part: |
| self.DEB("BOUNDARY ON") |
| self._current_part += 1 |
| self._current_pos = i + 1 + PARAMS_NUMBER |
| break |
| else: |
| skip_params = 1 |
| self.DEB("BOUNDARY OFF") |
| self._current_part += 1 |
| |
| elif token == "<TMPL_INCLUDE": |
| # TMPL_INCLUDE is left in the compiled template only |
| # when it was not replaced by the parser. |
| skip_params = 1 |
| filename = tokens[i + PARAM_NAME] |
| out += """ |
| <br /> |
| <p> |
| <strong>HTMLTMPL WARNING:</strong><br /> |
| Cannot include template: <strong>%s</strong> |
| </p> |
| <br /> |
| """ % filename |
| self.DEB("CANNOT INCLUDE WARNING") |
| |
| elif token == "<TMPL_GETTEXT": |
| skip_params = 1 |
| if DISABLE_OUTPUT not in output_control: |
| text = tokens[i + PARAM_GETTEXT_STRING] |
| out += gettext.gettext(text) |
| self.DEB("GETTEXT: " + text) |
| |
| else: |
| # Unknown processing directive. |
| raise TemplateError, "Invalid statement %s>." % token |
| |
| elif DISABLE_OUTPUT not in output_control: |
| # Raw textual template data. |
| # If output of current block is not disabled, then |
| # append template data to the output buffer. |
| out += token |
| |
| i += 1 |
| # end of the big while loop |
| |
| # Check whether all opening statements were closed. |
| if loop_name: raise TemplateError, "Missing </TMPL_LOOP>." |
| if output_control: raise TemplateError, "Missing </TMPL_IF> or </TMPL_UNLESS>" |
| return out |
| |
| ############################################## |
| # PRIVATE METHODS # |
| ############################################## |
| |
| def DEB(self, str): |
| """ Print debugging message to stderr if debugging is enabled. |
| @hidden |
| """ |
| if self._debug: print >> sys.stderr, str |
| |
| def find_value(self, var, loop_name, loop_pass, loop_total, |
| global_override=None): |
| """ Search the self._vars data structure to find variable var |
| located in currently processed pass of a loop which |
| is currently being processed. If the variable is an ordinary |
| variable, then return it. |
| |
| If the variable is an identificator of a loop, then |
| return the total number of times this loop will |
| be executed. |
| |
| Return an empty string, if the variable is not |
| found at all. |
| |
| @hidden |
| """ |
| # Search for the requested variable in magic vars if the name |
| # of the variable starts with "__" and if we are inside a loop. |
| if self._magic_vars and var.startswith("__") and loop_name: |
| return self.magic_var(var, loop_pass[-1], loop_total[-1]) |
| |
| # Search for an ordinary variable or for a loop. |
| # Recursively search in self._vars for the requested variable. |
| scope = self._vars |
| globals = [] |
| for i in range(len(loop_name)): |
| # If global lookup is on then push the value on the stack. |
| if ((self._global_vars and global_override != "0") or \ |
| global_override == "1") and scope.has_key(var) and \ |
| self.is_ordinary_var(scope[var]): |
| globals.append(scope[var]) |
| |
| # Descent deeper into the hierarchy. |
| if scope.has_key(loop_name[i]) and scope[loop_name[i]]: |
| scope = scope[loop_name[i]][loop_pass[i]] |
| else: |
| return "" |
| |
| if scope.has_key(var): |
| # Value exists in current loop. |
| if type(scope[var]) == ListType: |
| # The requested value is a loop. |
| # Return total number of its passes. |
| return len(scope[var]) |
| else: |
| return scope[var] |
| elif globals and \ |
| ((self._global_vars and global_override != "0") or \ |
| global_override == "1"): |
| # Return globally looked up value. |
| return globals.pop() |
| else: |
| # No value found. |
| if var[0].isupper(): |
| # This is a loop name. |
| # Return zero, because the user wants to know number |
| # of its passes. |
| return 0 |
| else: |
| return "" |
| |
| def magic_var(self, var, loop_pass, loop_total): |
| """ Resolve and return value of a magic variable. |
| Raise an exception if the magic variable is not recognized. |
| |
| @hidden |
| """ |
| self.DEB("MAGIC: '%s', PASS: %d, TOTAL: %d"\ |
| % (var, loop_pass, loop_total)) |
| if var == "__FIRST__": |
| if loop_pass == 0: |
| return 1 |
| else: |
| return 0 |
| elif var == "__LAST__": |
| if loop_pass == loop_total - 1: |
| return 1 |
| else: |
| return 0 |
| elif var == "__INNER__": |
| # If this is neither the first nor the last pass. |
| if loop_pass != 0 and loop_pass != loop_total - 1: |
| return 1 |
| else: |
| return 0 |
| elif var == "__PASS__": |
| # Magic variable __PASS__ counts passes from one. |
| return loop_pass + 1 |
| elif var == "__PASSTOTAL__": |
| return loop_total |
| elif var == "__ODD__": |
| # Internally pass numbers stored in loop_pass are counted from |
| # zero. But the template language presents them counted from one. |
| # Therefore we must add one to the actual loop_pass value to get |
| # the value we present to the user. |
| if (loop_pass + 1) % 2 != 0: |
| return 1 |
| else: |
| return 0 |
| elif var.startswith("__EVERY__"): |
| # Magic variable __EVERY__x is never true in first or last pass. |
| if loop_pass != 0 and loop_pass != loop_total - 1: |
| # Check if an integer follows the variable name. |
| try: |
| every = int(var[9:]) # nine is length of "__EVERY__" |
| except ValueError: |
| raise TemplateError, "Magic variable __EVERY__x: "\ |
| "Invalid pass number." |
| else: |
| if not every: |
| raise TemplateError, "Magic variable __EVERY__x: "\ |
| "Pass number cannot be zero." |
| elif (loop_pass + 1) % every == 0: |
| self.DEB("MAGIC: EVERY: " + str(every)) |
| return 1 |
| else: |
| return 0 |
| else: |
| return 0 |
| else: |
| raise TemplateError, "Invalid magic variable '%s'." % var |
| |
| def escape(self, str, override=""): |
| """ Escape a string either by HTML escaping or by URL escaping. |
| @hidden |
| """ |
| ESCAPE_QUOTES = 1 |
| if (self._html_escape and override != "NONE" and override != "0" and \ |
| override != "URL") or override == "HTML" or override == "1": |
| return cgi.escape(str, ESCAPE_QUOTES) |
| elif override == "URL": |
| return urllib.quote_plus(str) |
| else: |
| return str |
| |
| def is_ordinary_var(self, var): |
| """ Return true if var is a scalar. (not a reference to loop) |
| @hidden |
| """ |
| if type(var) == StringType or type(var) == IntType or \ |
| type(var) == LongType or type(var) == FloatType: |
| return 1 |
| else: |
| return 0 |
| |
| |
| ############################################## |
| # CLASS: TemplateCompiler # |
| ############################################## |
| |
| class TemplateCompiler: |
| """ Preprocess, parse, tokenize and compile the template. |
| |
| This class parses the template and produces a 'compiled' form |
| of it. This compiled form is an instance of the <em>Template</em> |
| class. The compiled form is used as input for the TemplateProcessor |
| which uses it to actually process the template. |
| |
| This class should be used direcly only when you need to compile |
| a template from a string. If your template is in a file, then you |
| should use the <em>TemplateManager</em> class which provides |
| a higher level interface to this class and also can save the |
| compiled template to disk in a precompiled form. |
| """ |
| |
| def __init__(self, include=1, max_include=5, comments=1, gettext=0, |
| debug=0): |
| """ Constructor. |
| |
| @header __init__(include=1, max_include=5, comments=1, gettext=0, |
| debug=0) |
| |
| @param include Enable or disable included templates. |
| @param max_include Maximum depth of nested inclusions. |
| @param comments Enable or disable template comments. |
| @param gettext Enable or disable gettext support. |
| @param debug Enable or disable debugging messages. |
| """ |
| |
| self._include = include |
| self._max_include = max_include |
| self._comments = comments |
| self._gettext = gettext |
| self._debug = debug |
| |
| # This is a list of filenames of all included templates. |
| # It's modified by the include_templates() method. |
| self._include_files = [] |
| |
| # This is a counter of current inclusion depth. It's used to prevent |
| # infinite recursive includes. |
| self._include_level = 0 |
| |
| def compile(self, file): |
| """ Compile template from a file. |
| |
| @header compile(file) |
| @return Compiled template. |
| The return value is an instance of the <em>Template</em> |
| class. |
| |
| @param file Filename of the template. |
| See the <em>prepare()</em> method of the <em>TemplateManager</em> |
| class for exaplanation of this parameter. |
| """ |
| |
| self.DEB("COMPILING FROM FILE: " + file) |
| self._include_path = os.path.join(os.path.dirname(file), INCLUDE_DIR) |
| tokens = self.parse(self.read(file)) |
| compile_params = (self._include, self._max_include, self._comments, |
| self._gettext) |
| return Template(__version__, file, self._include_files, |
| tokens, compile_params, self._debug) |
| |
| def compile_string(self, data): |
| """ Compile template from a string. |
| |
| This method compiles a template from a string. The |
| template cannot include any templates. |
| <strong>TMPL_INCLUDE</strong> statements are turned into warnings. |
| |
| @header compile_string(data) |
| @return Compiled template. |
| The return value is an instance of the <em>Template</em> |
| class. |
| |
| @param data String containing the template data. |
| """ |
| self.DEB("COMPILING FROM STRING") |
| self._include = 0 |
| tokens = self.parse(data) |
| compile_params = (self._include, self._max_include, self._comments, |
| self._gettext) |
| return Template(__version__, None, None, tokens, compile_params, |
| self._debug) |
| |
| ############################################## |
| # PRIVATE METHODS # |
| ############################################## |
| |
| def DEB(self, str): |
| """ Print debugging message to stderr if debugging is enabled. |
| @hidden |
| """ |
| if self._debug: print >> sys.stderr, str |
| |
| def read(self, filename): |
| """ Read content of file and return it. Raise an error if a problem |
| occurs. |
| @hidden |
| """ |
| self.DEB("READING: " + filename) |
| try: |
| f = None |
| try: |
| f = open(filename, "r") |
| data = f.read() |
| except IOError, (errno, errstr): |
| raise TemplateError, "IO error while reading template '%s': "\ |
| "(%d) %s" % (filename, errno, errstr) |
| else: |
| return data |
| finally: |
| if f: f.close() |
| |
| def parse(self, template_data): |
| """ Parse the template. This method is recursively called from |
| within the include_templates() method. |
| |
| @return List of processing tokens. |
| @hidden |
| """ |
| if self._comments: |
| self.DEB("PREPROCESS: COMMENTS") |
| template_data = self.remove_comments(template_data) |
| tokens = self.tokenize(template_data) |
| if self._include: |
| self.DEB("PREPROCESS: INCLUDES") |
| self.include_templates(tokens) |
| return tokens |
| |
| def remove_comments(self, template_data): |
| """ Remove comments from the template data. |
| @hidden |
| """ |
| pattern = r"### .*" |
| return re.sub(pattern, "", template_data) |
| |
| def include_templates(self, tokens): |
| """ Process TMPL_INCLUDE statements. Use the include_level counter |
| to prevent infinite recursion. Record paths to all included |
| templates to self._include_files. |
| @hidden |
| """ |
| i = 0 |
| out = "" # buffer for output |
| skip_params = 0 |
| |
| # Process the list of tokens. |
| while 1: |
| if i == len(tokens): break |
| if skip_params: |
| skip_params = 0 |
| i += PARAMS_NUMBER |
| continue |
| |
| token = tokens[i] |
| if token == "<TMPL_INCLUDE": |
| filename = tokens[i + PARAM_NAME] |
| if not filename: |
| raise TemplateError, "No filename in <TMPL_INCLUDE>." |
| self._include_level += 1 |
| if self._include_level > self._max_include: |
| # Do not include the template. |
| # Protection against infinite recursive includes. |
| skip_params = 1 |
| self.DEB("INCLUDE: LIMIT REACHED: " + filename) |
| else: |
| # Include the template. |
| skip_params = 0 |
| include_file = os.path.join(self._include_path, filename) |
| self._include_files.append(include_file) |
| include_data = self.read(include_file) |
| include_tokens = self.parse(include_data) |
| |
| # Append the tokens from the included template to actual |
| # position in the tokens list, replacing the TMPL_INCLUDE |
| # token and its parameters. |
| tokens[i:i+PARAMS_NUMBER+1] = include_tokens |
| i = i + len(include_tokens) |
| self.DEB("INCLUDED: " + filename) |
| continue # Do not increment 'i' below. |
| i += 1 |
| # end of the main while loop |
| |
| if self._include_level > 0: self._include_level -= 1 |
| return out |
| |
| def tokenize(self, template_data): |
| """ Split the template into tokens separated by template statements. |
| The statements itself and associated parameters are also |
| separately included in the resulting list of tokens. |
| Return list of the tokens. |
| |
| @hidden |
| """ |
| self.DEB("TOKENIZING TEMPLATE") |
| # NOTE: The TWO double quotes in character class in the regexp below |
| # are there only to prevent confusion of syntax highlighter in Emacs. |
| pattern = r""" |
| (?:^[ \t]+)? # eat spaces, tabs (opt.) |
| (< |
| (?:!--[ ])? # comment start + space (opt.) |
| /?TMPL_[A-Z]+ # closing slash / (opt.) + statement |
| [ a-zA-Z0-9""/.=:_\\-]* # this spans also comments ending (--) |
| >) |
| [%s]? # eat trailing newline (opt.) |
| """ % os.linesep |
| rc = re.compile(pattern, re.VERBOSE | re.MULTILINE) |
| split = rc.split(template_data) |
| tokens = [] |
| for statement in split: |
| if statement.startswith("<TMPL_") or \ |
| statement.startswith("</TMPL_") or \ |
| statement.startswith("<!-- TMPL_") or \ |
| statement.startswith("<!-- /TMPL_"): |
| # Processing statement. |
| statement = self.strip_brackets(statement) |
| params = re.split(r"\s+", statement) |
| tokens.append(self.find_directive(params)) |
| tokens.append(self.find_name(params)) |
| tokens.append(self.find_param("ESCAPE", params)) |
| tokens.append(self.find_param("GLOBAL", params)) |
| else: |
| # "Normal" template data. |
| if self._gettext: |
| self.DEB("PARSING GETTEXT STRINGS") |
| self.gettext_tokens(tokens, statement) |
| else: |
| tokens.append(statement) |
| return tokens |
| |
| def gettext_tokens(self, tokens, str): |
| """ Find gettext strings and return appropriate array of |
| processing tokens. |
| @hidden |
| """ |
| escaped = 0 |
| gt_mode = 0 |
| i = 0 |
| buf = "" |
| while(1): |
| if i == len(str): break |
| if str[i] == "\\": |
| escaped = 0 |
| if str[i+1] == "\\": |
| buf += "\\" |
| i += 2 |
| continue |
| elif str[i+1] == "[" or str[i+1] == "]": |
| escaped = 1 |
| else: |
| buf += "\\" |
| elif str[i] == "[" and str[i+1] == "[": |
| if gt_mode: |
| if escaped: |
| escaped = 0 |
| buf += "[" |
| else: |
| buf += "[" |
| else: |
| if escaped: |
| escaped = 0 |
| buf += "[" |
| else: |
| tokens.append(buf) |
| buf = "" |
| gt_mode = 1 |
| i += 2 |
| continue |
| elif str[i] == "]" and str[i+1] == "]": |
| if gt_mode: |
| if escaped: |
| escaped = 0 |
| buf += "]" |
| else: |
| self.add_gettext_token(tokens, buf) |
| buf = "" |
| gt_mode = 0 |
| i += 2 |
| continue |
| else: |
| if escaped: |
| escaped = 0 |
| buf += "]" |
| else: |
| buf += "]" |
| else: |
| escaped = 0 |
| buf += str[i] |
| i += 1 |
| # end of the loop |
| |
| if buf: |
| tokens.append(buf) |
| |
| def add_gettext_token(self, tokens, str): |
| """ Append a gettext token and gettext string to the tokens array. |
| @hidden |
| """ |
| self.DEB("GETTEXT PARSER: TOKEN: " + str) |
| tokens.append("<TMPL_GETTEXT") |
| tokens.append(str) |
| tokens.append(None) |
| tokens.append(None) |
| |
| def strip_brackets(self, statement): |
| """ Strip HTML brackets (with optional HTML comments) from the |
| beggining and from the end of a statement. |
| @hidden |
| """ |
| if statement.startswith("<!-- TMPL_") or \ |
| statement.startswith("<!-- /TMPL_"): |
| return statement[5:-4] |
| else: |
| return statement[1:-1] |
| |
| def find_directive(self, params): |
| """ Extract processing directive (TMPL_*) from a statement. |
| @hidden |
| """ |
| directive = params[0] |
| del params[0] |
| self.DEB("TOKENIZER: DIRECTIVE: " + directive) |
| return "<" + directive |
| |
| def find_name(self, params): |
| """ Extract identifier from a statement. The identifier can be |
| specified both implicitely or explicitely as a 'NAME' parameter. |
| @hidden |
| """ |
| if len(params) > 0 and '=' not in params[0]: |
| # implicit identifier |
| name = params[0] |
| del params[0] |
| else: |
| # explicit identifier as a 'NAME' parameter |
| name = self.find_param("NAME", params) |
| self.DEB("TOKENIZER: NAME: " + str(name)) |
| return name |
| |
| def find_param(self, param, params): |
| """ Extract value of parameter from a statement. |
| @hidden |
| """ |
| for pair in params: |
| name, value = pair.split("=") |
| if not name or not value: |
| raise TemplateError, "Syntax error in template." |
| if name == param: |
| if value[0] == '"': |
| # The value is in double quotes. |
| ret_value = value[1:-1] |
| else: |
| # The value is without double quotes. |
| ret_value = value |
| self.DEB("TOKENIZER: PARAM: '%s' => '%s'" % (param, ret_value)) |
| return ret_value |
| else: |
| self.DEB("TOKENIZER: PARAM: '%s' => NOT DEFINED" % param) |
| return None |
| |
| |
| ############################################## |
| # CLASS: Template # |
| ############################################## |
| |
| class Template: |
| """ This class represents a compiled template. |
| |
| This class provides storage and methods for the compiled template |
| and associated metadata. It's serialized by pickle if we need to |
| save the compiled template to disk in a precompiled form. |
| |
| You should never instantiate this class directly. Always use the |
| <em>TemplateManager</em> or <em>TemplateCompiler</em> classes to |
| create the instances of this class. |
| |
| The only method which you can directly use is the <em>is_uptodate</em> |
| method. |
| """ |
| |
| def __init__(self, version, file, include_files, tokens, compile_params, |
| debug=0): |
| """ Constructor. |
| @hidden |
| """ |
| self._version = version |
| self._file = file |
| self._tokens = tokens |
| self._compile_params = compile_params |
| self._debug = debug |
| self._mtime = None |
| self._include_mtimes = {} |
| |
| if not file: |
| self.DEB("TEMPLATE WAS COMPILED FROM A STRING") |
| return |
| |
| # Save modifitcation time of the main template file. |
| if os.path.isfile(file): |
| self._mtime = os.path.getmtime(file) |
| else: |
| raise TemplateError, "Template: file does not exist: '%s'" % file |
| |
| # Save modificaton times of all included template files. |
| for inc_file in include_files: |
| if os.path.isfile(inc_file): |
| self._include_mtimes[inc_file] = os.path.getmtime(inc_file) |
| else: |
| raise TemplateError, "Template: file does not exist: '%s'"\ |
| % inc_file |
| |
| self.DEB("NEW TEMPLATE CREATED") |
| |
| def is_uptodate(self, compile_params=None): |
| """ Check whether the compiled template is uptodate. |
| |
| Return true if this compiled template is uptodate. |
| Return false, if the template source file was changed on the |
| disk since it was compiled. |
| Works by comparison of modification times. |
| Also takes modification times of all included templates |
| into account. |
| |
| @header is_uptodate(compile_params=None) |
| @return True if the template is uptodate, false otherwise. |
| |
| @param compile_params Only for internal use. |
| Do not use this optional parameter. It's intended only for |
| internal use by the <em>TemplateManager</em>. |
| """ |
| if not self._file: |
| self.DEB("TEMPLATE COMPILED FROM A STRING") |
| return 0 |
| |
| if self._version != __version__: |
| self.DEB("TEMPLATE: VERSION NOT UPTODATE") |
| return 0 |
| |
| if compile_params != None and compile_params != self._compile_params: |
| self.DEB("TEMPLATE: DIFFERENT COMPILATION PARAMS") |
| return 0 |
| |
| # Check modification times of the main template and all included |
| # templates. If the included template no longer exists, then |
| # the problem will be resolved when the template is recompiled. |
| |
| # Main template file. |
| if not (os.path.isfile(self._file) and \ |
| self._mtime == os.path.getmtime(self._file)): |
| self.DEB("TEMPLATE: NOT UPTODATE: " + self._file) |
| return 0 |
| |
| # Included templates. |
| for inc_file in self._include_mtimes.keys(): |
| if not (os.path.isfile(inc_file) and \ |
| self._include_mtimes[inc_file] == \ |
| os.path.getmtime(inc_file)): |
| self.DEB("TEMPLATE: NOT UPTODATE: " + inc_file) |
| return 0 |
| else: |
| self.DEB("TEMPLATE: UPTODATE") |
| return 1 |
| |
| def tokens(self): |
| """ Get tokens of this template. |
| @hidden |
| """ |
| return self._tokens |
| |
| def file(self): |
| """ Get filename of the main file of this template. |
| @hidden |
| """ |
| return self._file |
| |
| def debug(self, debug): |
| """ Get debugging state. |
| @hidden |
| """ |
| self._debug = debug |
| |
| ############################################## |
| # PRIVATE METHODS # |
| ############################################## |
| |
| def __getstate__(self): |
| """ Used by pickle when the class is serialized. |
| Remove the 'debug' attribute before serialization. |
| @hidden |
| """ |
| dict = copy.copy(self.__dict__) |
| del dict["_debug"] |
| return dict |
| |
| def __setstate__(self, dict): |
| """ Used by pickle when the class is unserialized. |
| Add the 'debug' attribute. |
| @hidden |
| """ |
| dict["_debug"] = 0 |
| self.__dict__ = dict |
| |
| |
| def DEB(self, str): |
| """ Print debugging message to stderr. |
| @hidden |
| """ |
| if self._debug: print >> sys.stderr, str |
| |
| |
| ############################################## |
| # EXCEPTIONS # |
| ############################################## |
| |
| class TemplateError(Exception): |
| """ Fatal exception. Raised on runtime or template syntax errors. |
| |
| This exception is raised when a runtime error occurs or when a syntax |
| error in the template is found. It has one parameter which always |
| is a string containing a description of the error. |
| |
| All potential IOError exceptions are handled by the module and are |
| converted to TemplateError exceptions. That means you should catch the |
| TemplateError exception if there is a possibility that for example |
| the template file will not be accesssible. |
| |
| The exception can be raised by constructors or by any method of any |
| class. |
| |
| The instance is no longer usable when this exception is raised. |
| """ |
| |
| def __init__(self, error): |
| """ Constructor. |
| @hidden |
| """ |
| Exception.__init__(self, "Htmltmpl error: " + error) |
| |
| |
| class PrecompiledError(Exception): |
| """ This exception is _PRIVATE_ and non fatal. |
| @hidden |
| """ |
| |
| def __init__(self, template): |
| """ Constructor. |
| @hidden |
| """ |
| Exception.__init__(self, template) |
| |