| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- # 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 <alletam@gmail.com>
- #
- # Copyright (c) 2009 joonis new media
- # Author: Thimo Kraemer <thimo.kraemer@joonis.de>
- #
- # 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 "<string>", "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)
|