renderer.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. # -*- coding: utf-8 -*-
  2. import io
  3. try:
  4. from collections.abc import Sequence, Iterator, Callable
  5. except ImportError: # python 2
  6. from collections import Sequence, Iterator, Callable
  7. try:
  8. from .tokenizer import tokenize
  9. except (ValueError, SystemError): # python 2
  10. from tokenizer import tokenize
  11. import sys
  12. if sys.version_info[0] == 3:
  13. python3 = True
  14. unicode_type = str
  15. string_type = str
  16. def unicode(x, y):
  17. return x
  18. else: # python 2
  19. python3 = False
  20. unicode_type = unicode
  21. string_type = basestring # noqa: F821 (This is defined in python2)
  22. #
  23. # Helper functions
  24. #
  25. def _html_escape(string):
  26. """HTML escape all of these " & < >"""
  27. html_codes = {
  28. '"': '&quot;',
  29. '<': '&lt;',
  30. '>': '&gt;',
  31. }
  32. # & must be handled first
  33. string = string.replace('&', '&amp;')
  34. for char in html_codes:
  35. string = string.replace(char, html_codes[char])
  36. return string
  37. def _get_key(key, scopes):
  38. """Get a key from the current scope"""
  39. # If the key is a dot
  40. if key == '.':
  41. # Then just return the current scope
  42. return scopes[0]
  43. # Loop through the scopes
  44. for scope in scopes:
  45. try:
  46. # For every dot seperated key
  47. for child in key.split('.'):
  48. # Move into the scope
  49. try:
  50. # Try subscripting (Normal dictionaries)
  51. scope = scope[child]
  52. except (TypeError, AttributeError):
  53. # Try the dictionary (Complex types)
  54. scope = scope.__dict__[child]
  55. # Return an empty string if falsy, with two exceptions
  56. # 0 should return 0, and False should return False
  57. if scope in (0, False):
  58. return scope
  59. try:
  60. # This allows for custom falsy data types
  61. # https://github.com/noahmorrison/chevron/issues/35
  62. if scope._CHEVRON_return_scope_when_falsy:
  63. return scope
  64. except AttributeError:
  65. return scope or ''
  66. except (AttributeError, KeyError):
  67. # We couldn't find the key in the current scope
  68. # We'll try again on the next pass
  69. pass
  70. # We couldn't find the key in any of the scopes
  71. return ''
  72. def _get_partial(name, partials_dict, partials_path, partials_ext):
  73. """Load a partial"""
  74. try:
  75. # Maybe the partial is in the dictionary
  76. return partials_dict[name]
  77. except KeyError:
  78. # Nope...
  79. try:
  80. # Maybe it's in the file system
  81. path_ext = ('.' + partials_ext if partials_ext else '')
  82. path = partials_path + '/' + name + path_ext
  83. with io.open(path, 'r', encoding='utf-8') as partial:
  84. return partial.read()
  85. except IOError:
  86. # Alright I give up on you
  87. return ''
  88. #
  89. # The main rendering function
  90. #
  91. g_token_cache = {}
  92. def render(template='', data={}, partials_path='.', partials_ext='mustache',
  93. partials_dict={}, padding=0, def_ldel='{{', def_rdel='}}',
  94. scopes=None):
  95. """Render a mustache template.
  96. Renders a mustache template with a data scope and partial capability.
  97. Given the file structure...
  98. ├─╼ main.py
  99. ├─╼ main.ms
  100. └─┮ partials
  101. └── part.ms
  102. then main.py would make the following call:
  103. render(open('main.ms', 'r'), {...}, 'partials', 'ms')
  104. Arguments:
  105. template -- A file-like object or a string containing the template
  106. data -- A python dictionary with your data scope
  107. partials_path -- The path to where your partials are stored
  108. (defaults to '.')
  109. partials_ext -- The extension that you want the parser to look for
  110. (defaults to 'mustache')
  111. partials_dict -- A python dictionary which will be search for partials
  112. before the filesystem is. {'include': 'foo'} is the same
  113. as a file called include.mustache
  114. (defaults to {})
  115. padding -- This is for padding partials, and shouldn't be used
  116. (but can be if you really want to)
  117. def_ldel -- The default left delimiter
  118. ("{{" by default, as in spec compliant mustache)
  119. def_rdel -- The default right delimiter
  120. ("}}" by default, as in spec compliant mustache)
  121. scopes -- The list of scopes that get_key will look through
  122. Returns:
  123. A string containing the rendered template.
  124. """
  125. # If the template is a seqeuence but not derived from a string
  126. if isinstance(template, Sequence) and \
  127. not isinstance(template, string_type):
  128. # Then we don't need to tokenize it
  129. # But it does need to be a generator
  130. tokens = (token for token in template)
  131. else:
  132. if template in g_token_cache:
  133. tokens = (token for token in g_token_cache[template])
  134. else:
  135. # Otherwise make a generator
  136. tokens = tokenize(template, def_ldel, def_rdel)
  137. output = unicode('', 'utf-8')
  138. if scopes is None:
  139. scopes = [data]
  140. # Run through the tokens
  141. for tag, key in tokens:
  142. # Set the current scope
  143. current_scope = scopes[0]
  144. # If we're an end tag
  145. if tag == 'end':
  146. # Pop out of the latest scope
  147. scopes = scopes[1:]
  148. # If the current scope is falsy and not the only scope
  149. elif not current_scope and len(scopes) != 1:
  150. if tag in ['section', 'inverted section']:
  151. # Set the most recent scope to a falsy value
  152. # (I heard False is a good one)
  153. scopes.insert(0, False)
  154. # If we're a literal tag
  155. elif tag == 'literal':
  156. # Add padding to the key and add it to the output
  157. if not isinstance(key, unicode_type):
  158. key = unicode(key, 'utf-8')
  159. output += key.replace('\n', '\n' + (' ' * padding))
  160. # If we're a variable tag
  161. elif tag == 'variable':
  162. # Add the html escaped key to the output
  163. thing = _get_key(key, scopes)
  164. if thing is True and key == '.':
  165. # if we've coerced into a boolean by accident
  166. # (inverted tags do this)
  167. # then get the un-coerced object (next in the stack)
  168. thing = scopes[1]
  169. if not isinstance(thing, unicode_type):
  170. thing = unicode(str(thing), 'utf-8')
  171. output += _html_escape(thing)
  172. # If we're a no html escape tag
  173. elif tag == 'no escape':
  174. # Just lookup the key and add it
  175. thing = _get_key(key, scopes)
  176. if not isinstance(thing, unicode_type):
  177. thing = unicode(str(thing), 'utf-8')
  178. output += thing
  179. # If we're a section tag
  180. elif tag == 'section':
  181. # Get the sections scope
  182. scope = _get_key(key, scopes)
  183. # If the scope is a callable (as described in
  184. # https://mustache.github.io/mustache.5.html)
  185. if isinstance(scope, Callable):
  186. # Generate template text from tags
  187. text = unicode('', 'utf-8')
  188. tags = []
  189. for tag in tokens:
  190. if tag == ('end', key):
  191. break
  192. tags.append(tag)
  193. tag_type, tag_key = tag
  194. if tag_type == 'literal':
  195. text += tag_key
  196. elif tag_type == 'no escape':
  197. text += "%s& %s %s" % (def_ldel, tag_key, def_rdel)
  198. else:
  199. text += "%s%s %s%s" % (def_ldel, {
  200. 'commment': '!',
  201. 'section': '#',
  202. 'inverted section': '^',
  203. 'end': '/',
  204. 'partial': '>',
  205. 'set delimiter': '=',
  206. 'no escape': '&',
  207. 'variable': ''
  208. }[tag_type], tag_key, def_rdel)
  209. g_token_cache[text] = tags
  210. rend = scope(text, lambda template, data=None: render(template,
  211. data={},
  212. partials_path=partials_path,
  213. partials_ext=partials_ext,
  214. partials_dict=partials_dict,
  215. padding=padding,
  216. def_ldel=def_ldel, def_rdel=def_rdel,
  217. scopes=data and [data]+scopes or scopes))
  218. if python3:
  219. output += rend
  220. else: # python 2
  221. output += rend.decode('utf-8')
  222. # If the scope is a sequence, an iterator or generator but not
  223. # derived from a string
  224. elif isinstance(scope, (Sequence, Iterator)) and \
  225. not isinstance(scope, string_type):
  226. # Then we need to do some looping
  227. # Gather up all the tags inside the section
  228. # (And don't be tricked by nested end tags with the same key)
  229. # TODO: This feels like it still has edge cases, no?
  230. tags = []
  231. tags_with_same_key = 0
  232. for tag in tokens:
  233. if tag == ('section', key):
  234. tags_with_same_key += 1
  235. if tag == ('end', key):
  236. tags_with_same_key -= 1
  237. if tags_with_same_key < 0:
  238. break
  239. tags.append(tag)
  240. # For every item in the scope
  241. for thing in scope:
  242. # Append it as the most recent scope and render
  243. new_scope = [thing] + scopes
  244. rend = render(template=tags, scopes=new_scope,
  245. partials_path=partials_path,
  246. partials_ext=partials_ext,
  247. partials_dict=partials_dict,
  248. def_ldel=def_ldel, def_rdel=def_rdel)
  249. if python3:
  250. output += rend
  251. else: # python 2
  252. output += rend.decode('utf-8')
  253. else:
  254. # Otherwise we're just a scope section
  255. scopes.insert(0, scope)
  256. # If we're an inverted section
  257. elif tag == 'inverted section':
  258. # Add the flipped scope to the scopes
  259. scope = _get_key(key, scopes)
  260. scopes.insert(0, not scope)
  261. # If we're a partial
  262. elif tag == 'partial':
  263. # Load the partial
  264. partial = _get_partial(key, partials_dict,
  265. partials_path, partials_ext)
  266. # Find how much to pad the partial
  267. left = output.split('\n')[-1]
  268. part_padding = padding
  269. if left.isspace():
  270. part_padding += left.count(' ')
  271. # Render the partial
  272. part_out = render(template=partial, partials_path=partials_path,
  273. partials_ext=partials_ext,
  274. partials_dict=partials_dict,
  275. def_ldel=def_ldel, def_rdel=def_rdel,
  276. padding=part_padding, scopes=scopes)
  277. # If the partial was indented
  278. if left.isspace():
  279. # then remove the spaces from the end
  280. part_out = part_out.rstrip(' ')
  281. # Add the partials output to the ouput
  282. if python3:
  283. output += part_out
  284. else: # python 2
  285. output += part_out.decode('utf-8')
  286. if python3:
  287. return output
  288. else: # python 2
  289. return output.encode('utf-8')