made template independent of everything else (ready to bedome its own package) and other minor fixes

This commit is contained in:
mdipierro
2019-02-10 21:10:25 -08:00
parent d16b90c04f
commit e1cbaf1cb3
4 changed files with 1157 additions and 95 deletions
+147 -84
View File
@@ -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