microWebTemplate.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. """
  2. The MIT License (MIT)
  3. Copyright © 2018 Jean-Christophe Bos & HC² (www.hc2.fr)
  4. """
  5. import re
  6. class MicroWebTemplate :
  7. # ============================================================================
  8. # ===( Constants )============================================================
  9. # ============================================================================
  10. TOKEN_OPEN = '{{'
  11. TOKEN_CLOSE = '}}'
  12. TOKEN_OPEN_LEN = len(TOKEN_OPEN)
  13. TOKEN_CLOSE_LEN = len(TOKEN_CLOSE)
  14. INSTRUCTION_PYTHON = 'py'
  15. INSTRUCTION_IF = 'if'
  16. INSTRUCTION_ELIF = 'elif'
  17. INSTRUCTION_ELSE = 'else'
  18. INSTRUCTION_FOR = 'for'
  19. INSTRUCTION_END = 'end'
  20. INSTRUCTION_INCLUDE = 'include'
  21. MESSAGE_TEXT = ''
  22. MESSAGE_STYLE = ''
  23. # ============================================================================
  24. # ===( Constructor )==========================================================
  25. # ============================================================================
  26. def __init__(self, code, escapeStrFunc=None, filepath='') :
  27. self._code = code
  28. self._escapeStrFunc = escapeStrFunc
  29. self._filepath = filepath
  30. self._pos = 0
  31. self._endPos = len(code)-1
  32. self._line = 1
  33. self._reIdentifier = re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*$')
  34. self._pyGlobalVars = { }
  35. self._pyLocalVars = { }
  36. self._rendered = ''
  37. self._instructions = {
  38. MicroWebTemplate.INSTRUCTION_PYTHON : self._processInstructionPYTHON,
  39. MicroWebTemplate.INSTRUCTION_IF : self._processInstructionIF,
  40. MicroWebTemplate.INSTRUCTION_ELIF : self._processInstructionELIF,
  41. MicroWebTemplate.INSTRUCTION_ELSE : self._processInstructionELSE,
  42. MicroWebTemplate.INSTRUCTION_FOR : self._processInstructionFOR,
  43. MicroWebTemplate.INSTRUCTION_END : self._processInstructionEND,
  44. MicroWebTemplate.INSTRUCTION_INCLUDE: self._processInstructionINCLUDE,
  45. }
  46. # ============================================================================
  47. # ===( Functions )============================================================
  48. # ============================================================================
  49. def Validate(self, pyGlobalVars=None, pyLocalVars=None) :
  50. try :
  51. self._parseCode(pyGlobalVars, pyLocalVars, execute=False)
  52. return None
  53. except Exception as ex :
  54. return str(ex)
  55. # ----------------------------------------------------------------------------
  56. def Execute(self, pyGlobalVars=None, pyLocalVars=None) :
  57. try :
  58. self._parseCode(pyGlobalVars, pyLocalVars, execute=True)
  59. return self._rendered
  60. except Exception as ex :
  61. raise Exception(str(ex))
  62. # ============================================================================
  63. # ===( Utils )===============================================================
  64. # ============================================================================
  65. def _parseCode(self, pyGlobalVars, pyLocalVars, execute) :
  66. if pyGlobalVars:
  67. self._pyGlobalVars.update(pyGlobalVars)
  68. if pyLocalVars:
  69. self._pyLocalVars.update(pyLocalVars)
  70. self._pyLocalVars['MESSAGE_TEXT'] = MicroWebTemplate.MESSAGE_TEXT
  71. self._pyLocalVars['MESSAGE_STYLE'] = MicroWebTemplate.MESSAGE_STYLE
  72. self._rendered = ''
  73. newTokenToProcess = self._parseBloc(execute)
  74. if newTokenToProcess is not None :
  75. raise Exception( '"%s" instruction is not valid here (line %s)'
  76. % (newTokenToProcess, self._line) )
  77. MicroWebTemplate.MESSAGE_TEXT = ''
  78. MicroWebTemplate.MESSAGE_STYLE = ''
  79. # ----------------------------------------------------------------------------
  80. def _parseBloc(self, execute) :
  81. while self._pos <= self._endPos :
  82. c = self._code[self._pos]
  83. if c == MicroWebTemplate.TOKEN_OPEN[0] and \
  84. self._code[ self._pos : self._pos + MicroWebTemplate.TOKEN_OPEN_LEN ] == MicroWebTemplate.TOKEN_OPEN :
  85. self._pos += MicroWebTemplate.TOKEN_OPEN_LEN
  86. tokenContent = ''
  87. x = self._pos
  88. while True :
  89. if x > self._endPos :
  90. raise Exception("%s is missing (line %s)" % (MicroWebTemplate.TOKEN_CLOSE, self._line))
  91. c = self._code[x]
  92. if c == MicroWebTemplate.TOKEN_CLOSE[0] and \
  93. self._code[ x : x + MicroWebTemplate.TOKEN_CLOSE_LEN ] == MicroWebTemplate.TOKEN_CLOSE :
  94. self._pos = x + MicroWebTemplate.TOKEN_CLOSE_LEN
  95. break
  96. elif c == '\n' :
  97. self._line += 1
  98. tokenContent += c
  99. x += 1
  100. newTokenToProcess = self._processToken(tokenContent, execute)
  101. if newTokenToProcess is not None :
  102. return newTokenToProcess
  103. continue
  104. elif c == '\n' :
  105. self._line += 1
  106. if execute :
  107. self._rendered += c
  108. self._pos += 1
  109. return None
  110. # ----------------------------------------------------------------------------
  111. def _processToken(self, tokenContent, execute) :
  112. tokenContent = tokenContent.strip()
  113. parts = tokenContent.split(' ', 1)
  114. instructName = parts[0].strip()
  115. instructBody = parts[1].strip() if len(parts) > 1 else None
  116. if len(instructName) == 0 :
  117. raise Exception( '"%s %s" : instruction is missing (line %s)'
  118. % (MicroWebTemplate.TOKEN_OPEN, MicroWebTemplate.TOKEN_CLOSE, self._line) )
  119. newTokenToProcess = None
  120. if instructName in self._instructions :
  121. newTokenToProcess = self._instructions[instructName](instructBody, execute)
  122. elif execute :
  123. try :
  124. s = str( eval( tokenContent,
  125. self._pyGlobalVars,
  126. self._pyLocalVars ) )
  127. if (self._escapeStrFunc is not None) :
  128. self._rendered += self._escapeStrFunc(s)
  129. else :
  130. self._rendered += s
  131. except Exception as ex :
  132. raise Exception('%s (line %s)' % (str(ex), self._line))
  133. return newTokenToProcess
  134. # ----------------------------------------------------------------------------
  135. def _processInstructionPYTHON(self, instructionBody, execute) :
  136. if instructionBody is not None :
  137. raise Exception( 'Instruction "%s" is invalid (line %s)'
  138. % (MicroWebTemplate.INSTRUCTION_PYTHON, self._line) )
  139. pyCode = ''
  140. while True :
  141. if self._pos > self._endPos :
  142. raise Exception( '"%s" instruction is missing (line %s)'
  143. % (MicroWebTemplate.INSTRUCTION_END, self._line) )
  144. c = self._code[self._pos]
  145. if c == MicroWebTemplate.TOKEN_OPEN[0] and \
  146. self._code[ self._pos : self._pos + MicroWebTemplate.TOKEN_OPEN_LEN ] == MicroWebTemplate.TOKEN_OPEN :
  147. self._pos += MicroWebTemplate.TOKEN_OPEN_LEN
  148. tokenContent = ''
  149. x = self._pos
  150. while True :
  151. if x > self._endPos :
  152. raise Exception("%s is missing (line %s)" % (MicroWebTemplate.TOKEN_CLOSE, self._line))
  153. c = self._code[x]
  154. if c == MicroWebTemplate.TOKEN_CLOSE[0] and \
  155. self._code[ x : x + MicroWebTemplate.TOKEN_CLOSE_LEN ] == MicroWebTemplate.TOKEN_CLOSE :
  156. self._pos = x + MicroWebTemplate.TOKEN_CLOSE_LEN
  157. break
  158. elif c == '\n' :
  159. self._line += 1
  160. tokenContent += c
  161. x += 1
  162. tokenContent = tokenContent.strip()
  163. if tokenContent == MicroWebTemplate.INSTRUCTION_END :
  164. break
  165. raise Exception( '"%s" is a bad instruction in a python bloc (line %s)'
  166. % (tokenContent, self._line) )
  167. elif c == '\n' :
  168. self._line += 1
  169. if execute :
  170. pyCode += c
  171. self._pos += 1
  172. if execute :
  173. lines = pyCode.split('\n')
  174. indent = ''
  175. for line in lines :
  176. if len(line.strip()) > 0 :
  177. for c in line :
  178. if c == ' ' or c == '\t' :
  179. indent += c
  180. else :
  181. break
  182. break
  183. pyCode = ''
  184. for line in lines :
  185. if line.find(indent) == 0 :
  186. line = line[len(indent):]
  187. pyCode += line + '\n'
  188. try :
  189. exec(pyCode, self._pyGlobalVars, self._pyLocalVars)
  190. except Exception as ex :
  191. raise Exception('%s (line %s)' % (str(ex), self._line))
  192. return None
  193. # ----------------------------------------------------------------------------
  194. def _processInstructionIF(self, instructionBody, execute) :
  195. if instructionBody is not None :
  196. if execute :
  197. try :
  198. if (' ' not in instructionBody) and \
  199. ('=' not in instructionBody) and \
  200. ('<' not in instructionBody) and \
  201. ('>' not in instructionBody) and \
  202. (instructionBody not in self._pyGlobalVars) and \
  203. (instructionBody not in self._pyLocalVars):
  204. result = False
  205. else:
  206. result = bool(eval(instructionBody, self._pyGlobalVars, self._pyLocalVars))
  207. except Exception as ex :
  208. raise Exception('%s (line %s)' % (str(ex), self._line))
  209. else :
  210. result = False
  211. newTokenToProcess = self._parseBloc(execute and result)
  212. if newTokenToProcess is not None :
  213. if newTokenToProcess == MicroWebTemplate.INSTRUCTION_END :
  214. return None
  215. elif newTokenToProcess == MicroWebTemplate.INSTRUCTION_ELSE :
  216. newTokenToProcess = self._parseBloc(execute and not result)
  217. if newTokenToProcess is not None :
  218. if newTokenToProcess == MicroWebTemplate.INSTRUCTION_END :
  219. return None
  220. raise Exception( '"%s" instruction waited (line %s)'
  221. % (MicroWebTemplate.INSTRUCTION_END, self._line) )
  222. raise Exception( '"%s" instruction is missing (line %s)'
  223. % (MicroWebTemplate.INSTRUCTION_END, self._line) )
  224. elif newTokenToProcess == MicroWebTemplate.INSTRUCTION_ELIF :
  225. self._processInstructionIF(self._elifInstructionBody, execute and not result)
  226. return None
  227. raise Exception( '"%s" instruction waited (line %s)'
  228. % (MicroWebTemplate.INSTRUCTION_END, self._line) )
  229. raise Exception( '"%s" instruction is missing (line %s)'
  230. % (MicroWebTemplate.INSTRUCTION_END, self._line) )
  231. raise Exception( '"%s" alone is an incomplete syntax (line %s)'
  232. % (MicroWebTemplate.INSTRUCTION_IF, self._line) )
  233. # ----------------------------------------------------------------------------
  234. def _processInstructionELIF(self, instructionBody, execute) :
  235. if instructionBody is None :
  236. raise Exception( '"%s" alone is an incomplete syntax (line %s)'
  237. % (MicroWebTemplate.INSTRUCTION_ELIF, self._line) )
  238. self._elifInstructionBody = instructionBody
  239. return MicroWebTemplate.INSTRUCTION_ELIF
  240. # ----------------------------------------------------------------------------
  241. def _processInstructionELSE(self, instructionBody, execute) :
  242. if instructionBody is not None :
  243. raise Exception( 'Instruction "%s" is invalid (line %s)'
  244. % (MicroWebTemplate.INSTRUCTION_ELSE, self._line) )
  245. return MicroWebTemplate.INSTRUCTION_ELSE
  246. # ----------------------------------------------------------------------------
  247. def _processInstructionFOR(self, instructionBody, execute) :
  248. if instructionBody is not None :
  249. parts = instructionBody.split(' ', 1)
  250. identifier = parts[0].strip()
  251. if self._reIdentifier.match(identifier) is not None and len(parts) > 1 :
  252. parts = parts[1].strip().split(' ', 1)
  253. if parts[0] == 'in' and len(parts) > 1 :
  254. expression = parts[1].strip()
  255. newTokenToProcess = None
  256. beforePos = self._pos
  257. if execute :
  258. try :
  259. result = eval(expression, self._pyGlobalVars, self._pyLocalVars)
  260. except :
  261. raise Exception('%s (line %s)' % (str(expression), self._line))
  262. if execute and len(result) > 0 :
  263. for x in result :
  264. self._pyLocalVars[identifier] = x
  265. self._pos = beforePos
  266. newTokenToProcess = self._parseBloc(True)
  267. if newTokenToProcess != MicroWebTemplate.INSTRUCTION_END :
  268. break
  269. else :
  270. newTokenToProcess = self._parseBloc(False)
  271. if newTokenToProcess is not None :
  272. if newTokenToProcess == MicroWebTemplate.INSTRUCTION_END :
  273. return None
  274. raise Exception( '"%s" instruction waited (line %s)'
  275. % (MicroWebTemplate.INSTRUCTION_END, self._line) )
  276. raise Exception( '"%s" instruction is missing (line %s)'
  277. % (MicroWebTemplate.INSTRUCTION_END, self._line) )
  278. raise Exception( '"%s %s" is an invalid syntax'
  279. % (MicroWebTemplate.INSTRUCTION_FOR, instructionBody) )
  280. raise Exception( '"%s" alone is an incomplete syntax (line %s)'
  281. % (MicroWebTemplate.INSTRUCTION_FOR, self._line) )
  282. # ----------------------------------------------------------------------------
  283. def _processInstructionEND(self, instructionBody, execute) :
  284. if instructionBody is not None :
  285. raise Exception( 'Instruction "%s" is invalid (line %s)'
  286. % (MicroWebTemplate.INSTRUCTION_END, self._line) )
  287. return MicroWebTemplate.INSTRUCTION_END
  288. # ----------------------------------------------------------------------------
  289. def _processInstructionINCLUDE(self, instructionBody, execute) :
  290. if not instructionBody :
  291. raise Exception( '"%s" alone is an incomplete syntax (line %s)' % (MicroWebTemplate.INSTRUCTION_INCLUDE, self._line) )
  292. filename = instructionBody.replace('"','').replace("'",'').strip()
  293. idx = self._filepath.rindex('/')
  294. if idx >= 0 :
  295. filename = self._filepath[:idx+1] + filename
  296. with open(filename, 'r') as file :
  297. includeCode = file.read()
  298. self._code = self._code[:self._pos] + includeCode + self._code[self._pos:]
  299. self._endPos += len(includeCode)
  300. # ============================================================================
  301. # ============================================================================
  302. # ============================================================================