# Templite++ # A light-weight, fully functional, general purpose templating engine # Proudly made of shit and sticks. Strictly not for production use. # Extremly unsafe and difficult to debug. # # Copyright (c) 2022 Flipper Devices # Author: Aleksandr Kutuzov # # Copyright (c) 2009 joonis new media # Author: Thimo Kraemer # # Based on Templite by Tomer Filiba # http://code.activestate.com/recipes/496702/ # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. from enum import Enum import sys import os class TempliteCompiler: class State(Enum): TEXT = 1 CONTROL = 2 VARIABLE = 3 def __init__(self, source: str, encoding: str): self.blocks = [f"# -*- coding: {encoding} -*-"] self.block = "" self.source = source self.cursor = 0 self.offset = 0 def processText(self): self.block = self.block.replace("\\", "\\\\").replace('"', '\\"') self.block = "\t" * self.offset + f'write("""{self.block}""")' self.blocks.append(self.block) self.block = "" def getLine(self): return self.source[: self.cursor].count("\n") + 1 def controlIsEnding(self): block_stripped = self.block.lstrip() if block_stripped.startswith(":"): if not self.offset: raise SyntaxError( f"Line: {self.getLine()}, no statement to terminate: `{block_stripped}`" ) self.offset -= 1 self.block = block_stripped[1:] if not self.block.endswith(":"): return True return False def processControl(self): self.block = self.block.rstrip() if self.controlIsEnding(): self.block = "" return lines = self.block.splitlines() margin = min(len(line) - len(line.lstrip()) for line in lines if line.strip()) self.block = "\n".join("\t" * self.offset + line[margin:] for line in lines) self.blocks.append(self.block) if self.block.endswith(":"): self.offset += 1 self.block = "" def processVariable(self): self.block = self.block.strip() self.block = "\t" * self.offset + f"write({self.block})" self.blocks.append(self.block) self.block = "" def compile(self): state = self.State.TEXT # Process template source while self.cursor < len(self.source): # Process plain text till first token occurance if state == self.State.TEXT: if self.source[self.cursor :].startswith("{%"): state = self.State.CONTROL self.cursor += 1 elif self.source[self.cursor :].startswith("{{"): state = self.State.VARIABLE self.cursor += 1 else: self.block += self.source[self.cursor] # Commit self.block if token was found if state != self.State.TEXT: self.processText() elif state == self.State.CONTROL: if self.source[self.cursor :].startswith("%}"): self.cursor += 1 state = self.State.TEXT self.processControl() else: self.block += self.source[self.cursor] elif state == self.State.VARIABLE: if self.source[self.cursor :].startswith("}}"): self.cursor += 1 state = self.State.TEXT self.processVariable() else: self.block += self.source[self.cursor] else: raise Exception("Unknown State") self.cursor += 1 if state != self.State.TEXT: raise Exception("Last self.block was not closed") if self.block: self.processText() return "\n".join(self.blocks) class Templite: cache = {} def __init__(self, text=None, filename=None, encoding="utf-8", caching=False): """Loads a template from string or file.""" if filename: filename = os.path.abspath(filename) mtime = os.path.getmtime(filename) self.file = key = filename elif text is not None: self.file = mtime = None key = hash(text) else: raise ValueError("either text or filename required") # set attributes self.encoding = encoding self.caching = caching # check cache cache = self.cache if caching and key in cache and cache[key][0] == mtime: self._code = cache[key][1] return # read file if filename: with open(filename) as fh: text = fh.read() # Compile template to executable code = TempliteCompiler(text, self.encoding).compile() self._code = compile(code, self.file or "", "exec") # Cache for future use if caching: cache[key] = (mtime, self._code) def render(self, **namespace): """Renders the template according to the given namespace.""" stack = [] namespace["__file__"] = self.file # add write method def write(*args): for value in args: stack.append(str(value)) namespace["write"] = write # add include method def include(file): if not os.path.isabs(file): if self.file: base = os.path.dirname(self.file) else: base = os.path.dirname(sys.argv[0]) file = os.path.join(base, file) t = Templite(None, file, self.encoding, self.delimiters, self.caching) stack.append(t.render(**namespace)) namespace["include"] = include # execute template code exec(self._code, namespace) return "".join(stack)