made template independent of everything else (ready to bedome its own package) and other minor fixes
This commit is contained in:
+147
-84
@@ -2,23 +2,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
| This file is part of the web2py Web Framework
|
||||
| This file was extracted from the web2py Web Framework and made framework independent
|
||||
| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
|
||||
| Author: Thadeus Burgess
|
||||
| Contributors:
|
||||
| - Massimo Di Pierro for creating the original gluon/template.py
|
||||
| - Jonathan Lundell for extensively testing the regex on Jython.
|
||||
| - Limodou (creater of uliweb) who inspired the block-element support for web2py.
|
||||
|
||||
Templating syntax
|
||||
------------------
|
||||
"""
|
||||
|
||||
import os
|
||||
import cgi
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from functools import wraps
|
||||
from re import compile, sub, escape, DOTALL
|
||||
from gluon._compat import StringIO, unicodeT, to_unicode, to_bytes, to_native, basestring
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
|
||||
if PY2:
|
||||
from cgi import escape as html_escape
|
||||
from cStringIO import StringIO
|
||||
basestring = basestring
|
||||
unicodeT = unicode
|
||||
|
||||
def to_bytes(obj, charset='utf-8', errors='strict'):
|
||||
return bytes(obj) if isinstance(obj, (bytes, bytearray, buffer)) else obj.encode(charset, errors)
|
||||
|
||||
def to_native(obj, charset='utf8', errors='strict'):
|
||||
return obj if isinstance(obj, str) else obj.encode(charset, errors)
|
||||
|
||||
else:
|
||||
from html import escape as html_escape
|
||||
from io import StringIO, BytesIO
|
||||
basestring = str
|
||||
unicodeT = str
|
||||
|
||||
def to_bytes(obj, charset='utf-8', errors='strict'):
|
||||
return bytes(obj) if isinstance(obj, (bytes, bytearray, memoryview)) else obj.encode(charset, errors)
|
||||
|
||||
def to_native(obj, charset='utf8', errors='strict'):
|
||||
return obj if isinstance(obj, str) else obj.decode(charset, errors)
|
||||
|
||||
|
||||
DEFAULT_DELIMITERS = ('{{', '}}')
|
||||
|
||||
|
||||
def file_reader(filename, mode='rb'):
|
||||
try:
|
||||
with open(filename, mode) as fp:
|
||||
body = fp.read()
|
||||
return body
|
||||
except IOError:
|
||||
raise RestrictedError(filename, '', 'Unable to find the file')
|
||||
|
||||
try:
|
||||
# have web2py
|
||||
@@ -55,7 +85,6 @@ class SuperNode(Node):
|
||||
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):
|
||||
@@ -89,7 +118,7 @@ class BlockNode(Node):
|
||||
{{ end }}
|
||||
|
||||
"""
|
||||
def __init__(self, name='', pre_extend=False, delimiters=('{{', '}}')):
|
||||
def __init__(self, name='', pre_extend=False, delimiters=None):
|
||||
"""
|
||||
name - Name of this Node.
|
||||
"""
|
||||
@@ -230,10 +259,6 @@ class TemplateParser(object):
|
||||
template that need to be handled.
|
||||
|
||||
"""
|
||||
|
||||
default_delimiters = ('{{', '}}')
|
||||
r_tag = compile(r'(\{\{.*?\}\})', DOTALL)
|
||||
|
||||
r_multiline = compile(r'(""".*?""")|(\'\'\'.*?\'\'\')', DOTALL)
|
||||
|
||||
# These are used for re-indentation.
|
||||
@@ -247,18 +272,22 @@ class TemplateParser(object):
|
||||
|
||||
def __init__(self, text,
|
||||
name="ParserContainer",
|
||||
context=dict(),
|
||||
context=None,
|
||||
path='views/',
|
||||
writer='response.write',
|
||||
lexers={},
|
||||
delimiters=('{{', '}}'),
|
||||
_super_nodes = [],
|
||||
lexers=None,
|
||||
delimiters=None,
|
||||
_super_nodes = None,
|
||||
reader=None,
|
||||
):
|
||||
|
||||
context = context or {}
|
||||
# Keep a root level name.
|
||||
self.name = name
|
||||
# Raw text to start parsing.
|
||||
self.text = text
|
||||
# use the default reader
|
||||
self.reader = reader or file_reader
|
||||
# Writer to use (refer to the default for an example).
|
||||
# This will end up as
|
||||
# "%s(%s, escape=False)" % (self.writer, value)
|
||||
@@ -268,26 +297,19 @@ class TemplateParser(object):
|
||||
self.lexers = lexers
|
||||
else:
|
||||
self.lexers = {}
|
||||
if _super_nodes is None:
|
||||
_super_nodes = []
|
||||
if delimiters is None:
|
||||
delimiters = DEFAULT_DELIMITERS
|
||||
|
||||
# Path of templates
|
||||
self.path = path
|
||||
# Context for templates.
|
||||
self.context = context
|
||||
|
||||
# allow optional alternative 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) and (context['response'].delimiters != None):
|
||||
delimiters = context['response'].delimiters
|
||||
escaped_delimiters = (
|
||||
escape(delimiters[0]),
|
||||
escape(delimiters[1]))
|
||||
self.r_tag = compile(r'(%s.*?%s)' % escaped_delimiters,
|
||||
DOTALL)
|
||||
self.delimiters = delimiters
|
||||
escaped_delimiters = (escape(delimiters[0]), escape(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)
|
||||
@@ -442,9 +464,7 @@ class TemplateParser(object):
|
||||
|
||||
# try to read the text.
|
||||
try:
|
||||
fileobj = open(filepath, 'rb')
|
||||
text = fileobj.read()
|
||||
fileobj.close()
|
||||
text = self.reader(filepath)
|
||||
except IOError:
|
||||
self._raise_error('Unable to open included view file: ' + filepath)
|
||||
text = to_native(text)
|
||||
@@ -461,7 +481,8 @@ class TemplateParser(object):
|
||||
context=self.context,
|
||||
path=self.path,
|
||||
writer=self.writer,
|
||||
delimiters=self.delimiters)
|
||||
delimiters=self.delimiters,
|
||||
reader=self.reader)
|
||||
|
||||
content.append(t.content)
|
||||
|
||||
@@ -486,7 +507,8 @@ class TemplateParser(object):
|
||||
path=self.path,
|
||||
writer=self.writer,
|
||||
delimiters=self.delimiters,
|
||||
_super_nodes=super_nodes)
|
||||
_super_nodes=super_nodes,
|
||||
reader=self.reader)
|
||||
|
||||
# Make a temporary buffer that is unique for parent
|
||||
# template.
|
||||
@@ -548,7 +570,7 @@ class TemplateParser(object):
|
||||
# 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)
|
||||
ij = self.r_tag.split(to_native(text))
|
||||
# j = current index
|
||||
# i = current item
|
||||
stack = self.stack
|
||||
@@ -566,8 +588,7 @@ class TemplateParser(object):
|
||||
line = i
|
||||
|
||||
# Get rid of delimiters
|
||||
line = line[len(self.delimiters[0]): \
|
||||
-len(self.delimiters[1])].strip()
|
||||
line = line[len(self.delimiters[0]): -len(self.delimiters[1])].strip()
|
||||
|
||||
# This is bad juju, but let's do it anyway
|
||||
if not line:
|
||||
@@ -764,9 +785,10 @@ class TemplateParser(object):
|
||||
|
||||
def parse_template(filename,
|
||||
path='views/',
|
||||
context=dict(),
|
||||
lexers={},
|
||||
delimiters=('{{', '}}')
|
||||
context=None,
|
||||
lexers=None,
|
||||
delimiters=None,
|
||||
reader=None
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -776,28 +798,19 @@ def parse_template(filename,
|
||||
lexers: dict of custom lexers to use
|
||||
delimiters: opening and closing tags
|
||||
"""
|
||||
|
||||
context = context or {}
|
||||
lexers = lexers or {}
|
||||
delimiters = delimiters or DEFAULT_DELIMITERS
|
||||
reader = reader or file_reader
|
||||
# First, if we have a str try to open the file
|
||||
if isinstance(filename, basestring):
|
||||
fname = os.path.join(path, filename)
|
||||
try:
|
||||
with open(fname, 'rb') as fp:
|
||||
text = fp.read()
|
||||
except IOError:
|
||||
raise RestrictedError(filename, '', 'Unable to find the file')
|
||||
text = file_reader(fname)
|
||||
else:
|
||||
text = filename.read()
|
||||
text = to_native(text)
|
||||
# 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))
|
||||
return str(TemplateParser(text, context=context, path=path, lexers=lexers, delimiters=delimiters, reader=reader))
|
||||
|
||||
|
||||
class DummyResponse():
|
||||
@@ -815,7 +828,7 @@ class DummyResponse():
|
||||
data = str(data)
|
||||
elif isinstance(data, unicodeT):
|
||||
data = data.encode('utf8', 'xmlcharrefreplace')
|
||||
data = cgi.escape(data, True).replace("'", "'")
|
||||
data = html_escape(data, True).replace("'", "'")
|
||||
self.body.write(data)
|
||||
|
||||
|
||||
@@ -833,14 +846,15 @@ class NOESCAPE():
|
||||
# Here for integration with gluon.
|
||||
|
||||
|
||||
def render(content="hello world",
|
||||
def render(content=None,
|
||||
stream=None,
|
||||
filename=None,
|
||||
path=None,
|
||||
context={},
|
||||
lexers={},
|
||||
delimiters=('{{', '}}'),
|
||||
writer='response.write'
|
||||
context=None,
|
||||
lexers=None,
|
||||
delimiters='{{ }}',
|
||||
writer='response.write',
|
||||
reader=None
|
||||
):
|
||||
"""
|
||||
Generic render function
|
||||
@@ -882,6 +896,26 @@ def render(content="hello world",
|
||||
'012'
|
||||
|
||||
"""
|
||||
|
||||
# If we don't have anything to render, why bother?
|
||||
if content is None and stream is None and filename is None:
|
||||
raise SyntaxError("Must specify a stream or filename or content")
|
||||
|
||||
# handle defaults
|
||||
if context is None:
|
||||
context = {}
|
||||
if lexers is None:
|
||||
lexers = {}
|
||||
if isinstance(delimiters, basestring):
|
||||
delimiters = delimiters.split(' ',1)
|
||||
if not reader:
|
||||
reader = file_reader
|
||||
|
||||
# allow optional alternative delimiters
|
||||
if hasattr(context.get('response', None), 'delimiters'):
|
||||
if context['response'].delimiters is not None:
|
||||
delimiters = context['response'].delimiters
|
||||
|
||||
# here to avoid circular Imports
|
||||
try:
|
||||
from gluon.globals import Response
|
||||
@@ -904,23 +938,23 @@ def render(content="hello world",
|
||||
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(to_native(content))
|
||||
if content is None:
|
||||
if stream is not None:
|
||||
content = stream.read()
|
||||
elif filename is not None:
|
||||
content = reader(filename)
|
||||
else:
|
||||
content = '(no template found)'
|
||||
|
||||
# Execute the template.
|
||||
code = str(TemplateParser(stream.read(
|
||||
), context=context, path=path, lexers=lexers, delimiters=delimiters, writer=writer))
|
||||
code = str(TemplateParser(text=content,
|
||||
context=context,
|
||||
path=path,
|
||||
lexers=lexers,
|
||||
delimiters=delimiters,
|
||||
writer=writer,
|
||||
reader=reader))
|
||||
|
||||
try:
|
||||
exec(code, context)
|
||||
@@ -928,11 +962,40 @@ def render(content="hello world",
|
||||
# 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
|
||||
|
||||
class template(object):
|
||||
|
||||
def __init__(self, filename='{name}.html', body=None, path=None, lexers=None, delimiters=None, reader=None):
|
||||
self.filename = filename
|
||||
self.body = body
|
||||
self.path = path
|
||||
self.lexers = lexers
|
||||
self.delimiters = delimiters
|
||||
self.reader = reader or file_reader
|
||||
|
||||
def __call__(self, func):
|
||||
@wraps(func)
|
||||
def wrapper(*a, **b):
|
||||
context = func(*a, **b)
|
||||
if isinstance(context, dict):
|
||||
filename = self.filename.format(name=func.__name__)
|
||||
if self.body:
|
||||
body = self.body
|
||||
else:
|
||||
body = self.reader(filename)
|
||||
return render(
|
||||
content=body,
|
||||
path=self.path,
|
||||
lexers=self.lexers,
|
||||
delimiters=self.delimiters,
|
||||
context=context,
|
||||
reader=self.reader)
|
||||
else:
|
||||
return context
|
||||
return wrapper
|
||||
|
||||
|
||||
Reference in New Issue
Block a user