From 7c893e093bde4ccc076093cc68248d9ec7345962 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sun, 4 Apr 2021 19:35:35 +0200 Subject: [PATCH] feat: Import simple templating engine Signed-off-by: Lucas Schwiderski --- addon/bitsquid/step.py | 135 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 addon/bitsquid/step.py diff --git a/addon/bitsquid/step.py b/addon/bitsquid/step.py new file mode 100644 index 0000000..ffcd956 --- /dev/null +++ b/addon/bitsquid/step.py @@ -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, "", "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, "", "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 = "&<>\"'", ["&", "<", ">", """, "'"] + 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