templite.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. # Templite++
  2. # A light-weight, fully functional, general purpose templating engine
  3. # Proudly made of shit and sticks. Strictly not for production use.
  4. # Extremly unsafe and difficult to debug.
  5. #
  6. # Copyright (c) 2022 Flipper Devices
  7. # Author: Aleksandr Kutuzov <alletam@gmail.com>
  8. #
  9. # Copyright (c) 2009 joonis new media
  10. # Author: Thimo Kraemer <thimo.kraemer@joonis.de>
  11. #
  12. # Based on Templite by Tomer Filiba
  13. # http://code.activestate.com/recipes/496702/
  14. #
  15. # This program is free software; you can redistribute it and/or modify
  16. # it under the terms of the GNU General Public License as published by
  17. # the Free Software Foundation; either version 2 of the License, or
  18. # (at your option) any later version.
  19. #
  20. # This program is distributed in the hope that it will be useful,
  21. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  23. # GNU General Public License for more details.
  24. #
  25. # You should have received a copy of the GNU General Public License
  26. # along with this program; if not, write to the Free Software
  27. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  28. # MA 02110-1301, USA.
  29. from enum import Enum
  30. import sys
  31. import os
  32. class TempliteCompiler:
  33. class State(Enum):
  34. TEXT = 1
  35. CONTROL = 2
  36. VARIABLE = 3
  37. def __init__(self, source: str, encoding: str):
  38. self.blocks = [f"# -*- coding: {encoding} -*-"]
  39. self.block = ""
  40. self.source = source
  41. self.cursor = 0
  42. self.offset = 0
  43. def processText(self):
  44. self.block = self.block.replace("\\", "\\\\").replace('"', '\\"')
  45. self.block = "\t" * self.offset + f'write("""{self.block}""")'
  46. self.blocks.append(self.block)
  47. self.block = ""
  48. def getLine(self):
  49. return self.source[: self.cursor].count("\n") + 1
  50. def controlIsEnding(self):
  51. block_stripped = self.block.lstrip()
  52. if block_stripped.startswith(":"):
  53. if not self.offset:
  54. raise SyntaxError(
  55. f"Line: {self.getLine()}, no statement to terminate: `{block_stripped}`"
  56. )
  57. self.offset -= 1
  58. self.block = block_stripped[1:]
  59. if not self.block.endswith(":"):
  60. return True
  61. return False
  62. def processControl(self):
  63. self.block = self.block.rstrip()
  64. if self.controlIsEnding():
  65. self.block = ""
  66. return
  67. lines = self.block.splitlines()
  68. margin = min(len(l) - len(l.lstrip()) for l in lines if l.strip())
  69. self.block = "\n".join("\t" * self.offset + l[margin:] for l in lines)
  70. self.blocks.append(self.block)
  71. if self.block.endswith(":"):
  72. self.offset += 1
  73. self.block = ""
  74. def processVariable(self):
  75. self.block = self.block.strip()
  76. self.block = "\t" * self.offset + f"write({self.block})"
  77. self.blocks.append(self.block)
  78. self.block = ""
  79. def compile(self):
  80. state = self.State.TEXT
  81. # Process template source
  82. while self.cursor < len(self.source):
  83. # Process plain text till first token occurance
  84. if state == self.State.TEXT:
  85. if self.source[self.cursor :].startswith("{%"):
  86. state = self.State.CONTROL
  87. self.cursor += 1
  88. elif self.source[self.cursor :].startswith("{{"):
  89. state = self.State.VARIABLE
  90. self.cursor += 1
  91. else:
  92. self.block += self.source[self.cursor]
  93. # Commit self.block if token was found
  94. if state != self.State.TEXT:
  95. self.processText()
  96. elif state == self.State.CONTROL:
  97. if self.source[self.cursor :].startswith("%}"):
  98. self.cursor += 1
  99. state = self.State.TEXT
  100. self.processControl()
  101. else:
  102. self.block += self.source[self.cursor]
  103. elif state == self.State.VARIABLE:
  104. if self.source[self.cursor :].startswith("}}"):
  105. self.cursor += 1
  106. state = self.State.TEXT
  107. self.processVariable()
  108. else:
  109. self.block += self.source[self.cursor]
  110. else:
  111. raise Exception("Unknown State")
  112. self.cursor += 1
  113. if state != self.State.TEXT:
  114. raise Exception("Last self.block was not closed")
  115. if self.block:
  116. self.processText()
  117. return "\n".join(self.blocks)
  118. class Templite:
  119. cache = {}
  120. def __init__(self, text=None, filename=None, encoding="utf-8", caching=False):
  121. """Loads a template from string or file."""
  122. if filename:
  123. filename = os.path.abspath(filename)
  124. mtime = os.path.getmtime(filename)
  125. self.file = key = filename
  126. elif text is not None:
  127. self.file = mtime = None
  128. key = hash(text)
  129. else:
  130. raise ValueError("either text or filename required")
  131. # set attributes
  132. self.encoding = encoding
  133. self.caching = caching
  134. # check cache
  135. cache = self.cache
  136. if caching and key in cache and cache[key][0] == mtime:
  137. self._code = cache[key][1]
  138. return
  139. # read file
  140. if filename:
  141. with open(filename) as fh:
  142. text = fh.read()
  143. # Compile template to executable
  144. code = TempliteCompiler(text, self.encoding).compile()
  145. self._code = compile(code, self.file or "<string>", "exec")
  146. # Cache for future use
  147. if caching:
  148. cache[key] = (mtime, self._code)
  149. def render(self, **namespace):
  150. """Renders the template according to the given namespace."""
  151. stack = []
  152. namespace["__file__"] = self.file
  153. # add write method
  154. def write(*args):
  155. for value in args:
  156. stack.append(str(value))
  157. namespace["write"] = write
  158. # add include method
  159. def include(file):
  160. if not os.path.isabs(file):
  161. if self.file:
  162. base = os.path.dirname(self.file)
  163. else:
  164. base = os.path.dirname(sys.argv[0])
  165. file = os.path.join(base, file)
  166. t = Templite(None, file, self.encoding, self.delimiters, self.caching)
  167. stack.append(t.render(**namespace))
  168. namespace["include"] = include
  169. # execute template code
  170. exec(self._code, namespace)
  171. return "".join(stack)