1
Fork 0

feat: Import simple templating engine

Signed-off-by: Lucas Schwiderski <lucas@lschwiderski.de>
This commit is contained in:
Lucas Schwiderski 2021-04-04 19:35:35 +02:00
parent 716b68313b
commit 7c893e093b
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8

135
addon/bitsquid/step.py Normal file
View file

@ -0,0 +1,135 @@
"""A light and fast template engine."""
import re
import sys
PY3 = False
if sys.version_info > (3, 0):
PY3 = True
class Template(object):
COMPILED_TEMPLATES = {} # {template string: code object, }
# Regex for stripping all leading, trailing and interleaving whitespace.
RE_STRIP = re.compile("(^[ \t]+|[ \t]+$|(?<=[ \t])[ \t]+|\A[\r\n]+|[ \t\r\n]+\Z)", re.M)
def __init__(self, template, strip=True):
"""Initialize class"""
super(Template, self).__init__()
self.template = template
self.options = {"strip": strip}
self.builtins = {"escape": lambda s: escape_html(s),
"setopt": lambda k, v: self.options.update({k: v}), }
if template in Template.COMPILED_TEMPLATES:
self.code = Template.COMPILED_TEMPLATES[template]
else:
self.code = self._process(self._preprocess(self.template))
Template.COMPILED_TEMPLATES[template] = self.code
def expand(self, namespace={}, **kw):
"""Return the expanded template string"""
output = []
namespace.update(kw, **self.builtins)
namespace["echo"] = lambda s: output.append(s)
namespace["isdef"] = lambda v: v in namespace
eval(compile(self.code, "<string>", "exec"), namespace)
return self._postprocess("".join(map(to_unicode, output)))
def stream(self, buffer, namespace={}, encoding="utf-8", **kw):
"""Expand the template and stream it to a file-like buffer."""
def write_buffer(s, flush=False, cache = [""]):
# Cache output as a single string and write to buffer.
cache[0] += to_unicode(s)
if flush and cache[0] or len(cache[0]) > 65536:
buffer.write(postprocess(cache[0]))
cache[0] = ""
namespace.update(kw, **self.builtins)
namespace["echo"] = write_buffer
namespace["isdef"] = lambda v: v in namespace
postprocess = lambda s: s.encode(encoding)
if self.options["strip"]:
postprocess = lambda s: Template.RE_STRIP.sub("", s).encode(encoding)
eval(compile(self.code, "<string>", "exec"), namespace)
write_buffer("", flush=True) # Flush any last cached bytes
def _preprocess(self, template):
"""Modify template string before code conversion"""
# Replace inline ('%') blocks for easier parsing
o = re.compile("(?m)^[ \t]*%((if|for|while|try).+:)")
c = re.compile("(?m)^[ \t]*%(((else|elif|except|finally).*:)|(end\w+))")
template = c.sub(r"<%:\g<1>%>", o.sub(r"<%\g<1>%>", template))
# Replace ({{x}}) variables with '<%echo(x)%>'
v = re.compile("\{\{(.*?)\}\}")
template = v.sub(r"<%echo(\g<1>)%>\n", template)
return template
def _process(self, template):
"""Return the code generated from the template string"""
code_blk = re.compile(r"<%(.*?)%>\n?", re.DOTALL)
indent = 0
code = []
for n, blk in enumerate(code_blk.split(template)):
# Replace '<\%' and '%\>' escapes
blk = re.sub(r"<\\%", "<%", re.sub(r"%\\>", "%>", blk))
# Unescape '%{}' characters
blk = re.sub(r"\\(%|{|})", "\g<1>", blk)
if not (n % 2):
# Escape backslash characters
blk = re.sub(r'\\', r'\\\\', blk)
# Escape double-quote characters
blk = re.sub(r'"', r'\\"', blk)
blk = (" " * (indent*4)) + 'echo("""{0}""")'.format(blk)
else:
blk = blk.rstrip()
if blk.lstrip().startswith(":"):
if not indent:
err = "unexpected block ending"
raise SyntaxError("Line {0}: {1}".format(n, err))
indent -= 1
if blk.startswith(":end"):
continue
blk = blk.lstrip()[1:]
blk = re.sub("(?m)^", " " * (indent * 4), blk)
if blk.endswith(":"):
indent += 1
code.append(blk)
if indent:
err = "Reached EOF before closing block"
raise EOFError("Line {0}: {1}".format(n, err))
return "\n".join(code)
def _postprocess(self, output):
"""Modify output string after variables and code evaluation"""
if self.options["strip"]:
output = Template.RE_STRIP.sub("", output)
return output
def escape_html(x):
"""Escape HTML special characters &<> and quotes "'."""
CHARS, ENTITIES = "&<>\"'", ["&amp;", "&lt;", "&gt;", "&quot;", "&#39;"]
string = x if isinstance(x, basestring) else str(x)
for c, e in zip(CHARS, ENTITIES): string = string.replace(c, e)
return string
def to_unicode(x, encoding="utf-8"):
"""Convert anything to Unicode."""
if PY3:
return str(x)
if not isinstance(x, unicode):
x = unicode(str(x), encoding, errors="replace")
return x