/gluon/template.py
Python | 918 lines | 879 code | 15 blank | 24 comment | 14 complexity | 39ba9d98aa9836613657410e688efe40 MD5 | raw file
Possible License(s): BSD-2-Clause, MIT, BSD-3-Clause
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- """
- This file is part of the web2py Web Framework (Copyrighted, 2007-2011).
- License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
- Author: Thadeus Burgess
- Contributors:
- - Thank you to Massimo Di Pierro for creating the original gluon/template.py
- - Thank you to Jonathan Lundell for extensively testing the regex on Jython.
- - Thank you to Limodou (creater of uliweb) who inspired the block-element support for web2py.
- """
- import os
- import cgi
- import logging
- from re import compile, sub, escape, DOTALL
- try:
- import cStringIO as StringIO
- except:
- from io import StringIO
- try:
- # have web2py
- from restricted import RestrictedError
- from globals import current
- except ImportError:
- # do not have web2py
- current = None
- def RestrictedError(a, b, c):
- logging.error(str(a) + ':' + str(b) + ':' + str(c))
- return RuntimeError
- class Node(object):
- """
- Basic Container Object
- """
- def __init__(self, value=None, pre_extend=False):
- self.value = value
- self.pre_extend = pre_extend
- def __str__(self):
- return str(self.value)
- class SuperNode(Node):
- def __init__(self, name='', pre_extend=False):
- self.name = name
- self.value = None
- self.pre_extend = pre_extend
- def __str__(self):
- if self.value:
- return str(self.value)
- else:
- # raise SyntaxError("Undefined parent block ``%s``. \n" % self.name + "You must define a block before referencing it.\nMake sure you have not left out an ``{{end}}`` tag." )
- return ''
- def __repr__(self):
- return "%s->%s" % (self.name, self.value)
- def output_aux(node, blocks):
- # If we have a block level
- # If we can override this block.
- # Override block from vars.
- # Else we take the default
- # Else its just a string
- return (blocks[node.name].output(blocks)
- if node.name in blocks else
- node.output(blocks)) \
- if isinstance(node, BlockNode) \
- else str(node)
- class BlockNode(Node):
- """
- Block Container.
- This Node can contain other Nodes and will render in a hierarchical order
- of when nodes were added.
- ie::
- {{ block test }}
- This is default block test
- {{ end }}
- """
- def __init__(self, name='', pre_extend=False, delimiters=('{{', '}}')):
- """
- name - Name of this Node.
- """
- self.nodes = []
- self.name = name
- self.pre_extend = pre_extend
- self.left, self.right = delimiters
- def __repr__(self):
- lines = ['%sblock %s%s' % (self.left, self.name, self.right)]
- lines += [str(node) for node in self.nodes]
- lines.append('%send%s' % (self.left, self.right))
- return ''.join(lines)
- def __str__(self):
- """
- Get this BlockNodes content, not including child Nodes
- """
- return ''.join(str(node) for node in self.nodes
- if not isinstance(node, BlockNode))
- def append(self, node):
- """
- Add an element to the nodes.
- Keyword Arguments
- - node -- Node object or string to append.
- """
- if isinstance(node, str) or isinstance(node, Node):
- self.nodes.append(node)
- else:
- raise TypeError("Invalid type; must be instance of ``str`` or ``BlockNode``. %s" % node)
- def extend(self, other):
- """
- Extend the list of nodes with another BlockNode class.
- Keyword Arguments
- - other -- BlockNode or Content object to extend from.
- """
- if isinstance(other, BlockNode):
- self.nodes.extend(other.nodes)
- else:
- raise TypeError(
- "Invalid type; must be instance of ``BlockNode``. %s" % other)
- def output(self, blocks):
- """
- Merges all nodes into a single string.
- blocks -- Dictionary of blocks that are extending
- from this template.
- """
- return ''.join(output_aux(node, blocks) for node in self.nodes)
- class Content(BlockNode):
- """
- Parent Container -- Used as the root level BlockNode.
- Contains functions that operate as such.
- """
- def __init__(self, name="ContentBlock", pre_extend=False):
- """
- Keyword Arguments
- name -- Unique name for this BlockNode
- """
- self.name = name
- self.nodes = []
- self.blocks = {}
- self.pre_extend = pre_extend
- def __str__(self):
- return ''.join(output_aux(node, self.blocks) for node in self.nodes)
- def _insert(self, other, index=0):
- """
- Inserts object at index.
- """
- if isinstance(other, (str, Node)):
- self.nodes.insert(index, other)
- else:
- raise TypeError(
- "Invalid type, must be instance of ``str`` or ``Node``.")
- def insert(self, other, index=0):
- """
- Inserts object at index.
- You may pass a list of objects and have them inserted.
- """
- if isinstance(other, (list, tuple)):
- # Must reverse so the order stays the same.
- other.reverse()
- for item in other:
- self._insert(item, index)
- else:
- self._insert(other, index)
- def append(self, node):
- """
- Adds a node to list. If it is a BlockNode then we assign a block for it.
- """
- if isinstance(node, (str, Node)):
- self.nodes.append(node)
- if isinstance(node, BlockNode):
- self.blocks[node.name] = node
- else:
- raise TypeError("Invalid type, must be instance of ``str`` or ``BlockNode``. %s" % node)
- def extend(self, other):
- """
- Extends the objects list of nodes with another objects nodes
- """
- if isinstance(other, BlockNode):
- self.nodes.extend(other.nodes)
- self.blocks.update(other.blocks)
- else:
- raise TypeError(
- "Invalid type; must be instance of ``BlockNode``. %s" % other)
- def clear_content(self):
- self.nodes = []
- class TemplateParser(object):
- default_delimiters = ('{{', '}}')
- r_tag = compile(r'(\{\{.*?\}\})', DOTALL)
- r_multiline = compile(r'(""".*?""")|(\'\'\'.*?\'\'\')', DOTALL)
- # These are used for re-indentation.
- # Indent + 1
- re_block = compile('^(elif |else:|except:|except |finally:).*$', DOTALL)
- # Indent - 1
- re_unblock = compile('^(return|continue|break|raise)( .*)?$', DOTALL)
- # Indent - 1
- re_pass = compile('^pass( .*)?$', DOTALL)
- def __init__(self, text,
- name="ParserContainer",
- context=dict(),
- path='views/',
- writer='response.write',
- lexers={},
- delimiters=('{{', '}}'),
- _super_nodes = [],
- ):
- """
- text -- text to parse
- context -- context to parse in
- path -- folder path to templates
- writer -- string of writer class to use
- lexers -- dict of custom lexers to use.
- delimiters -- for example ('{{','}}')
- _super_nodes -- a list of nodes to check for inclusion
- this should only be set by "self.extend"
- It contains a list of SuperNodes from a child
- template that need to be handled.
- """
- # Keep a root level name.
- self.name = name
- # Raw text to start parsing.
- self.text = text
- # Writer to use (refer to the default for an example).
- # This will end up as
- # "%s(%s, escape=False)" % (self.writer, value)
- self.writer = writer
- # Dictionary of custom name lexers to use.
- if isinstance(lexers, dict):
- self.lexers = lexers
- else:
- self.lexers = {}
- # Path of templates
- self.path = path
- # Context for templates.
- self.context = context
- # allow optional alternative delimiters
- self.delimiters = delimiters
- if delimiters != self.default_delimiters:
- escaped_delimiters = (escape(delimiters[0]),
- escape(delimiters[1]))
- self.r_tag = compile(r'(%s.*?%s)' % escaped_delimiters, DOTALL)
- elif hasattr(context.get('response', None), 'delimiters'):
- if context['response'].delimiters != self.default_delimiters:
- escaped_delimiters = (
- escape(context['response'].delimiters[0]),
- escape(context['response'].delimiters[1]))
- self.r_tag = compile(r'(%s.*?%s)' % escaped_delimiters,
- DOTALL)
- # Create a root level Content that everything will go into.
- self.content = Content(name=name)
- # Stack will hold our current stack of nodes.
- # As we descend into a node, it will be added to the stack
- # And when we leave, it will be removed from the stack.
- # self.content should stay on the stack at all times.
- self.stack = [self.content]
- # This variable will hold a reference to every super block
- # that we come across in this template.
- self.super_nodes = []
- # This variable will hold a reference to the child
- # super nodes that need handling.
- self.child_super_nodes = _super_nodes
- # This variable will hold a reference to every block
- # that we come across in this template
- self.blocks = {}
- # Begin parsing.
- self.parse(text)
- def to_string(self):
- """
- Return the parsed template with correct indentation.
- Used to make it easier to port to python3.
- """
- return self.reindent(str(self.content))
- def __str__(self):
- "Make sure str works exactly the same as python 3"
- return self.to_string()
- def __unicode__(self):
- "Make sure str works exactly the same as python 3"
- return self.to_string()
- def reindent(self, text):
- """
- Reindents a string of unindented python code.
- """
- # Get each of our lines into an array.
- lines = text.split('\n')
- # Our new lines
- new_lines = []
- # Keeps track of how many indents we have.
- # Used for when we need to drop a level of indentation
- # only to reindent on the next line.
- credit = 0
- # Current indentation
- k = 0
- #################
- # THINGS TO KNOW
- #################
- # k += 1 means indent
- # k -= 1 means unindent
- # credit = 1 means unindent on the next line.
- for raw_line in lines:
- line = raw_line.strip()
- # ignore empty lines
- if not line:
- continue
- # If we have a line that contains python code that
- # should be unindented for this line of code.
- # and then reindented for the next line.
- if TemplateParser.re_block.match(line):
- k = k + credit - 1
- # We obviously can't have a negative indentation
- k = max(k, 0)
- # Add the indentation!
- new_lines.append(' ' * (4 * k) + line)
- # Bank account back to 0 again :(
- credit = 0
- # If we are a pass block, we obviously de-dent.
- if TemplateParser.re_pass.match(line):
- k -= 1
- # If we are any of the following, de-dent.
- # However, we should stay on the same level
- # But the line right after us will be de-dented.
- # So we add one credit to keep us at the level
- # while moving back one indentation level.
- if TemplateParser.re_unblock.match(line):
- credit = 1
- k -= 1
- # If we are an if statement, a try, or a semi-colon we
- # probably need to indent the next line.
- if line.endswith(':') and not line.startswith('#'):
- k += 1
- # This must come before so that we can raise an error with the
- # right content.
- new_text = '\n'.join(new_lines)
- if k > 0:
- self._raise_error('missing "pass" in view', new_text)
- elif k < 0:
- self._raise_error('too many "pass" in view', new_text)
- return new_text
- def _raise_error(self, message='', text=None):
- """
- Raise an error using itself as the filename and textual content.
- """
- raise RestrictedError(self.name, text or self.text, message)
- def _get_file_text(self, filename):
- """
- Attempt to open ``filename`` and retrieve its text.
- This will use self.path to search for the file.
- """
- # If they didn't specify a filename, how can we find one!
- if not filename.strip():
- self._raise_error('Invalid template filename')
- # Allow Views to include other views dynamically
- context = self.context
- if current and not "response" in context:
- context["response"] = getattr(current, 'response', None)
- # Get the filename; filename looks like ``"template.html"``.
- # We need to eval to remove the quotes and get the string type.
- filename = eval(filename, context)
- # Get the path of the file on the system.
- filepath = self.path and os.path.join(self.path, filename) or filename
- # try to read the text.
- try:
- fileobj = open(filepath, 'rb')
- text = fileobj.read()
- fileobj.close()
- except IOError:
- self._raise_error('Unable to open included view file: ' + filepath)
- return text
- def include(self, content, filename):
- """
- Include ``filename`` here.
- """
- text = self._get_file_text(filename)
- t = TemplateParser(text,
- name=filename,
- context=self.context,
- path=self.path,
- writer=self.writer,
- delimiters=self.delimiters)
- content.append(t.content)
- def extend(self, filename):
- """
- Extend ``filename``. Anything not declared in a block defined by the
- parent will be placed in the parent templates ``{{include}}`` block.
- """
- text = self._get_file_text(filename)
- # Create out nodes list to send to the parent
- super_nodes = []
- # We want to include any non-handled nodes.
- super_nodes.extend(self.child_super_nodes)
- # And our nodes as well.
- super_nodes.extend(self.super_nodes)
- t = TemplateParser(text,
- name=filename,
- context=self.context,
- path=self.path,
- writer=self.writer,
- delimiters=self.delimiters,
- _super_nodes=super_nodes)
- # Make a temporary buffer that is unique for parent
- # template.
- buf = BlockNode(
- name='__include__' + filename, delimiters=self.delimiters)
- pre = []
- # Iterate through each of our nodes
- for node in self.content.nodes:
- # If a node is a block
- if isinstance(node, BlockNode):
- # That happens to be in the parent template
- if node.name in t.content.blocks:
- # Do not include it
- continue
- if isinstance(node, Node):
- # Or if the node was before the extension
- # we should not include it
- if node.pre_extend:
- pre.append(node)
- continue
- # Otherwise, it should go int the
- # Parent templates {{include}} section.
- buf.append(node)
- else:
- buf.append(node)
- # Clear our current nodes. We will be replacing this with
- # the parent nodes.
- self.content.nodes = []
- t_content = t.content
- # Set our include, unique by filename
- t_content.blocks['__include__' + filename] = buf
- # Make sure our pre_extended nodes go first
- t_content.insert(pre)
- # Then we extend our blocks
- t_content.extend(self.content)
- # Work off the parent node.
- self.content = t_content
- def parse(self, text):
- # Basically, r_tag.split will split the text into
- # an array containing, 'non-tag', 'tag', 'non-tag', 'tag'
- # so if we alternate this variable, we know
- # what to look for. This is alternate to
- # line.startswith("{{")
- in_tag = False
- extend = None
- pre_extend = True
- # Use a list to store everything in
- # This is because later the code will "look ahead"
- # for missing strings or brackets.
- ij = self.r_tag.split(text)
- # j = current index
- # i = current item
- stack = self.stack
- for j in range(len(ij)):
- i = ij[j]
- if i:
- if not stack:
- self._raise_error('The "end" tag is unmatched, please check if you have a starting "block" tag')
- # Our current element in the stack.
- top = stack[-1]
- if in_tag:
- line = i
- # Get rid of delimiters
- line = line[len(self.delimiters[0]):-len(self.delimiters[1])].strip()
- # This is bad juju, but let's do it anyway
- if not line:
- continue
- # We do not want to replace the newlines in code,
- # only in block comments.
- def remove_newline(re_val):
- # Take the entire match and replace newlines with
- # escaped newlines.
- return re_val.group(0).replace('\n', '\\n')
- # Perform block comment escaping.
- # This performs escaping ON anything
- # in between """ and """
- line = sub(TemplateParser.r_multiline,
- remove_newline,
- line)
- if line.startswith('='):
- # IE: {{=response.title}}
- name, value = '=', line[1:].strip()
- else:
- v = line.split(' ', 1)
- if len(v) == 1:
- # Example
- # {{ include }}
- # {{ end }}
- name = v[0]
- value = ''
- else:
- # Example
- # {{ block pie }}
- # {{ include "layout.html" }}
- # {{ for i in range(10): }}
- name = v[0]
- value = v[1]
- # This will replace newlines in block comments
- # with the newline character. This is so that they
- # retain their formatting, but squish down to one
- # line in the rendered template.
- # First check if we have any custom lexers
- if name in self.lexers:
- # Pass the information to the lexer
- # and allow it to inject in the environment
- # You can define custom names such as
- # '{{<<variable}}' which could potentially
- # write unescaped version of the variable.
- self.lexers[name](parser=self,
- value=value,
- top=top,
- stack=stack)
- elif name == '=':
- # So we have a variable to insert into
- # the template
- buf = "\n%s(%s)" % (self.writer, value)
- top.append(Node(buf, pre_extend=pre_extend))
- elif name == 'block' and not value.startswith('='):
- # Make a new node with name.
- node = BlockNode(name=value.strip(),
- pre_extend=pre_extend,
- delimiters=self.delimiters)
- # Append this node to our active node
- top.append(node)
- # Make sure to add the node to the stack.
- # so anything after this gets added
- # to this node. This allows us to
- # "nest" nodes.
- stack.append(node)
- elif name == 'end' and not value.startswith('='):
- # We are done with this node.
- # Save an instance of it
- self.blocks[top.name] = top
- # Pop it.
- stack.pop()
- elif name == 'super' and not value.startswith('='):
- # Get our correct target name
- # If they just called {{super}} without a name
- # attempt to assume the top blocks name.
- if value:
- target_node = value
- else:
- target_node = top.name
- # Create a SuperNode instance
- node = SuperNode(name=target_node,
- pre_extend=pre_extend)
- # Add this to our list to be taken care of
- self.super_nodes.append(node)
- # And put in in the tree
- top.append(node)
- elif name == 'include' and not value.startswith('='):
- # If we know the target file to include
- if value:
- self.include(top, value)
- # Otherwise, make a temporary include node
- # That the child node will know to hook into.
- else:
- include_node = BlockNode(
- name='__include__' + self.name,
- pre_extend=pre_extend,
- delimiters=self.delimiters)
- top.append(include_node)
- elif name == 'extend' and not value.startswith('='):
- # We need to extend the following
- # template.
- extend = value
- pre_extend = False
- else:
- # If we don't know where it belongs
- # we just add it anyways without formatting.
- if line and in_tag:
- # Split on the newlines >.<
- tokens = line.split('\n')
- # We need to look for any instances of
- # for i in range(10):
- # = i
- # pass
- # So we can properly put a response.write() in place.
- continuation = False
- len_parsed = 0
- for k, token in enumerate(tokens):
- token = tokens[k] = token.strip()
- len_parsed += len(token)
- if token.startswith('='):
- if token.endswith('\\'):
- continuation = True
- tokens[k] = "\n%s(%s" % (
- self.writer, token[1:].strip())
- else:
- tokens[k] = "\n%s(%s)" % (
- self.writer, token[1:].strip())
- elif continuation:
- tokens[k] += ')'
- continuation = False
- buf = "\n%s" % '\n'.join(tokens)
- top.append(Node(buf, pre_extend=pre_extend))
- else:
- # It is HTML so just include it.
- buf = "\n%s(%r, escape=False)" % (self.writer, i)
- top.append(Node(buf, pre_extend=pre_extend))
- # Remember: tag, not tag, tag, not tag
- in_tag = not in_tag
- # Make a list of items to remove from child
- to_rm = []
- # Go through each of the children nodes
- for node in self.child_super_nodes:
- # If we declared a block that this node wants to include
- if node.name in self.blocks:
- # Go ahead and include it!
- node.value = self.blocks[node.name]
- # Since we processed this child, we don't need to
- # pass it along to the parent
- to_rm.append(node)
- # Remove some of the processed nodes
- for node in to_rm:
- # Since this is a pointer, it works beautifully.
- # Sometimes I miss C-Style pointers... I want my asterisk...
- self.child_super_nodes.remove(node)
- # If we need to extend a template.
- if extend:
- self.extend(extend)
- # We need this for integration with gluon
- def parse_template(filename,
- path='views/',
- context=dict(),
- lexers={},
- delimiters=('{{', '}}')
- ):
- """
- filename can be a view filename in the views folder or an input stream
- path is the path of a views folder
- context is a dictionary of symbols used to render the template
- """
- # First, if we have a str try to open the file
- if isinstance(filename, str):
- try:
- fp = open(os.path.join(path, filename), 'rb')
- text = fp.read()
- fp.close()
- except IOError:
- raise RestrictedError(filename, '', 'Unable to find the file')
- else:
- text = filename.read()
- # Use the file contents to get a parsed template and return it.
- return str(TemplateParser(text, context=context, path=path, lexers=lexers, delimiters=delimiters))
- def get_parsed(text):
- """
- Returns the indented python code of text. Useful for unit testing.
- """
- return str(TemplateParser(text))
- class DummyResponse():
- def __init__(self):
- self.body = StringIO.StringIO()
- def write(self, data, escape=True):
- if not escape:
- self.body.write(str(data))
- elif hasattr(data, 'xml') and callable(data.xml):
- self.body.write(data.xml())
- else:
- # make it a string
- if not isinstance(data, (str, unicode)):
- data = str(data)
- elif isinstance(data, unicode):
- data = data.encode('utf8', 'xmlcharrefreplace')
- data = cgi.escape(data, True).replace("'", "'")
- self.body.write(data)
- class NOESCAPE():
- """
- A little helper to avoid escaping.
- """
- def __init__(self, text):
- self.text = text
- def xml(self):
- return self.text
- # And this is a generic render function.
- # Here for integration with gluon.
- def render(content="hello world",
- stream=None,
- filename=None,
- path=None,
- context={},
- lexers={},
- delimiters=('{{', '}}'),
- writer='response.write'
- ):
- """
- >>> render()
- 'hello world'
- >>> render(content='abc')
- 'abc'
- >>> render(content='abc\\'')
- "abc'"
- >>> render(content='a"\\'bc')
- 'a"\\'bc'
- >>> render(content='a\\nbc')
- 'a\\nbc'
- >>> render(content='a"bcd"e')
- 'a"bcd"e'
- >>> render(content="'''a\\nc'''")
- "'''a\\nc'''"
- >>> render(content="'''a\\'c'''")
- "'''a\'c'''"
- >>> render(content='{{for i in range(a):}}{{=i}}<br />{{pass}}', context=dict(a=5))
- '0<br />1<br />2<br />3<br />4<br />'
- >>> render(content='{%for i in range(a):%}{%=i%}<br />{%pass%}', context=dict(a=5),delimiters=('{%','%}'))
- '0<br />1<br />2<br />3<br />4<br />'
- >>> render(content="{{='''hello\\nworld'''}}")
- 'hello\\nworld'
- >>> render(content='{{for i in range(3):\\n=i\\npass}}')
- '012'
- """
- # here to avoid circular Imports
- try:
- from globals import Response
- except ImportError:
- # Working standalone. Build a mock Response object.
- Response = DummyResponse
- # Add it to the context so we can use it.
- if not 'NOESCAPE' in context:
- context['NOESCAPE'] = NOESCAPE
- # save current response class
- if context and 'response' in context:
- old_response_body = context['response'].body
- context['response'].body = StringIO.StringIO()
- else:
- old_response_body = None
- context['response'] = Response()
- # If we don't have anything to render, why bother?
- if not content and not stream and not filename:
- raise SyntaxError("Must specify a stream or filename or content")
- # Here for legacy purposes, probably can be reduced to
- # something more simple.
- close_stream = False
- if not stream:
- if filename:
- stream = open(filename, 'rb')
- close_stream = True
- elif content:
- stream = StringIO.StringIO(content)
- # Execute the template.
- code = str(TemplateParser(stream.read(
- ), context=context, path=path, lexers=lexers, delimiters=delimiters, writer=writer))
- try:
- exec(code) in context
- except Exception:
- # for i,line in enumerate(code.split('\n')): print i,line
- raise
- if close_stream:
- stream.close()
- # Returned the rendered content.
- text = context['response'].body.getvalue()
- if old_response_body is not None:
- context['response'].body = old_response_body
- return text
- if __name__ == '__main__':
- import doctest
- doctest.testmod()