."
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 == "
HTMLTMPL WARNING:
Cannot include template: %s
""" % filename
self.DEB("CANNOT INCLUDE WARNING")
elif token == "." % 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 ."
if output_control: raise TemplateError, "Missing or "
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 Template
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 TemplateManager 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 Template
class.
@param file Filename of the template.
See the prepare() method of the TemplateManager
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.
TMPL_INCLUDE statements are turned into warnings.
@header compile_string(data)
@return Compiled template.
The return value is an instance of the Template
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 == "."
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(" 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
TemplateManager or TemplateCompiler classes to
create the instances of this class.
The only method which you can directly use is the is_uptodate
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 TemplateManager.
"""
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)