t_otemplates.nim 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. discard """
  2. output: "Success"
  3. """
  4. # Ref:
  5. # http://nim-lang.org/macros.html
  6. # http://nim-lang.org/parseutils.html
  7. # Imports
  8. import tables, parseutils, macros, strutils
  9. import annotate
  10. export annotate
  11. # Fields
  12. const identChars = {'a'..'z', 'A'..'Z', '0'..'9', '_'}
  13. # Procedure Declarations
  14. proc parse_template(node: NimNode, value: string) {.compiletime.}
  15. # Procedure Definitions
  16. proc substring(value: string, index: int, length = -1): string {.compiletime.} =
  17. ## Returns a string at most `length` characters long, starting at `index`.
  18. return if length < 0: value.substr(index)
  19. elif length == 0: ""
  20. else: value.substr(index, index + length-1)
  21. proc parse_thru_eol(value: string, index: int): int {.compiletime.} =
  22. ## Reads until and past the end of the current line, unless
  23. ## a non-whitespace character is encountered first
  24. var remainder: string
  25. var read = value.parseUntil(remainder, {0x0A.char}, index)
  26. if remainder.skipWhitespace() == read:
  27. return read + 1
  28. proc trim_after_eol(value: var string) {.compiletime.} =
  29. ## Trims any whitespace at end after \n
  30. var toTrim = 0
  31. for i in countdown(value.len-1, 0):
  32. # If \n, return
  33. if value[i] in [' ', '\t']: inc(toTrim)
  34. else: break
  35. if toTrim > 0:
  36. value = value.substring(0, value.len - toTrim)
  37. proc trim_eol(value: var string) {.compiletime.} =
  38. ## Removes everything after the last line if it contains nothing but whitespace
  39. for i in countdown(value.len - 1, 0):
  40. # If \n, trim and return
  41. if value[i] == 0x0A.char:
  42. value = value.substr(0, i)
  43. break
  44. # This is the first character
  45. if i == 0:
  46. value = ""
  47. break
  48. # Skip change
  49. if not (value[i] in [' ', '\t']): break
  50. proc detect_indent(value: string, index: int): int {.compiletime.} =
  51. ## Detects how indented the line at `index` is.
  52. # Seek to the beginning of the line.
  53. var lastChar = index
  54. for i in countdown(index, 0):
  55. if value[i] == 0x0A.char:
  56. # if \n, return the indentation level
  57. return lastChar - i
  58. elif not (value[i] in [' ', '\t']):
  59. # if non-whitespace char, decrement lastChar
  60. dec(lastChar)
  61. proc parse_thru_string(value: string, i: var int, strType = '"') {.compiletime.} =
  62. ## Parses until ending " or ' is reached.
  63. inc(i)
  64. if i < value.len-1:
  65. inc(i, value.skipUntil({'\\', strType}, i))
  66. proc parse_to_close(value: string, index: int, open='(', close=')', opened=0): int {.compiletime.} =
  67. ## Reads until all opened braces are closed
  68. ## ignoring any strings "" or ''
  69. var remainder = value.substring(index)
  70. var open_braces = opened
  71. result = 0
  72. while result < remainder.len:
  73. var c = remainder[result]
  74. if c == open: inc(open_braces)
  75. elif c == close: dec(open_braces)
  76. elif c == '"': remainder.parse_thru_string(result)
  77. elif c == '\'': remainder.parse_thru_string(result, '\'')
  78. if open_braces == 0: break
  79. else: inc(result)
  80. iterator parse_stmt_list(value: string, index: var int): string =
  81. ## Parses unguided ${..} block
  82. var read = value.parse_to_close(index, open='{', close='}')
  83. var expressions = value.substring(index + 1, read - 1).split({ ';', 0x0A.char })
  84. for expression in expressions:
  85. let value = expression.strip
  86. if value.len > 0:
  87. yield value
  88. #Increment index & parse thru EOL
  89. inc(index, read + 1)
  90. inc(index, value.parse_thru_eol(index))
  91. iterator parse_compound_statements(value, identifier: string, index: int): string =
  92. ## Parses through several statements, i.e. if {} elif {} else {}
  93. ## and returns the initialization of each as an empty statement
  94. ## i.e. if x == 5 { ... } becomes if x == 5: nil.
  95. template get_next_ident(expected) =
  96. var nextIdent: string
  97. discard value.parseWhile(nextIdent, {'$'} + identChars, i)
  98. var next: string
  99. var read: int
  100. if nextIdent == "case":
  101. # We have to handle case a bit differently
  102. read = value.parseUntil(next, '$', i)
  103. inc(i, read)
  104. yield next.strip(leading=false) & "\n"
  105. else:
  106. read = value.parseUntil(next, '{', i)
  107. if nextIdent in expected:
  108. inc(i, read)
  109. # Parse until closing }, then skip whitespace afterwards
  110. read = value.parse_to_close(i, open='{', close='}')
  111. inc(i, read + 1)
  112. inc(i, value.skipWhitespace(i))
  113. yield next & ": nil\n"
  114. else: break
  115. var i = index
  116. while true:
  117. # Check if next statement would be valid, given the identifier
  118. if identifier in ["if", "when"]:
  119. get_next_ident([identifier, "$elif", "$else"])
  120. elif identifier == "case":
  121. get_next_ident(["case", "$of", "$elif", "$else"])
  122. elif identifier == "try":
  123. get_next_ident(["try", "$except", "$finally"])
  124. proc parse_complex_stmt(value, identifier: string, index: var int): NimNode {.compiletime.} =
  125. ## Parses if/when/try /elif /else /except /finally statements
  126. # Build up complex statement string
  127. var stmtString = newString(0)
  128. var numStatements = 0
  129. for statement in value.parse_compound_statements(identifier, index):
  130. if statement[0] == '$': stmtString.add(statement.substr(1))
  131. else: stmtString.add(statement)
  132. inc(numStatements)
  133. # Parse stmt string
  134. result = parseExpr(stmtString)
  135. var resultIndex = 0
  136. # Fast forward a bit if this is a case statement
  137. if identifier == "case":
  138. inc(resultIndex)
  139. while resultIndex < numStatements:
  140. # Detect indentation
  141. let indent = detect_indent(value, index)
  142. # Parse until an open brace `{`
  143. var read = value.skipUntil('{', index)
  144. inc(index, read + 1)
  145. # Parse through EOL
  146. inc(index, value.parse_thru_eol(index))
  147. # Parse through { .. }
  148. read = value.parse_to_close(index, open='{', close='}', opened=1)
  149. # Add parsed sub-expression into body
  150. var body = newStmtList()
  151. var stmtString = value.substring(index, read)
  152. trim_after_eol(stmtString)
  153. stmtString = reindent(stmtString, indent)
  154. parse_template(body, stmtString)
  155. inc(index, read + 1)
  156. # Insert body into result
  157. var stmtIndex = result[resultIndex].len-1
  158. result[resultIndex][stmtIndex] = body
  159. # Parse through EOL again & increment result index
  160. inc(index, value.parse_thru_eol(index))
  161. inc(resultIndex)
  162. proc parse_simple_statement(value: string, index: var int): NimNode {.compiletime.} =
  163. ## Parses for/while
  164. # Detect indentation
  165. let indent = detect_indent(value, index)
  166. # Parse until an open brace `{`
  167. var splitValue: string
  168. var read = value.parseUntil(splitValue, '{', index)
  169. result = parseExpr(splitValue & ":nil")
  170. inc(index, read + 1)
  171. # Parse through EOL
  172. inc(index, value.parse_thru_eol(index))
  173. # Parse through { .. }
  174. read = value.parse_to_close(index, open='{', close='}', opened=1)
  175. # Add parsed sub-expression into body
  176. var body = newStmtList()
  177. var stmtString = value.substring(index, read)
  178. trim_after_eol(stmtString)
  179. stmtString = reindent(stmtString, indent)
  180. parse_template(body, stmtString)
  181. inc(index, read + 1)
  182. # Insert body into result
  183. var stmtIndex = result.len-1
  184. result[stmtIndex] = body
  185. # Parse through EOL again
  186. inc(index, value.parse_thru_eol(index))
  187. proc parse_until_symbol(node: NimNode, value: string, index: var int): bool {.compiletime.} =
  188. ## Parses a string until a $ symbol is encountered, if
  189. ## two $$'s are encountered in a row, a split will happen
  190. ## removing one of the $'s from the resulting output
  191. var splitValue: string
  192. var read = value.parseUntil(splitValue, '$', index)
  193. var insertionPoint = node.len
  194. inc(index, read + 1)
  195. if index < value.len:
  196. case value[index]
  197. of '$':
  198. # Check for duplicate `$`, meaning this is an escaped $
  199. node.add newCall("add", ident("result"), newStrLitNode("$"))
  200. inc(index)
  201. of '(':
  202. # Check for open `(`, which means parse as simple single-line expression.
  203. trim_eol(splitValue)
  204. read = value.parse_to_close(index) + 1
  205. node.add newCall("add", ident("result"),
  206. newCall(bindSym"strip", parseExpr("$" & value.substring(index, read)))
  207. )
  208. inc(index, read)
  209. of '{':
  210. # Check for open `{`, which means open statement list
  211. trim_eol(splitValue)
  212. for s in value.parse_stmt_list(index):
  213. node.add parseExpr(s)
  214. else:
  215. # Otherwise parse while valid `identChars` and make expression w/ $
  216. var identifier: string
  217. read = value.parseWhile(identifier, identChars, index)
  218. if identifier in ["for", "while"]:
  219. ## for/while means open simple statement
  220. trim_eol(splitValue)
  221. node.add value.parse_simple_statement(index)
  222. elif identifier in ["if", "when", "case", "try"]:
  223. ## if/when/case/try means complex statement
  224. trim_eol(splitValue)
  225. node.add value.parse_complex_stmt(identifier, index)
  226. elif identifier.len > 0:
  227. ## Treat as simple variable
  228. node.add newCall("add", ident("result"), newCall("$", ident(identifier)))
  229. inc(index, read)
  230. result = true
  231. # Insert
  232. if splitValue.len > 0:
  233. node.insert insertionPoint, newCall("add", ident("result"), newStrLitNode(splitValue))
  234. proc parse_template(node: NimNode, value: string) =
  235. ## Parses through entire template, outputing valid
  236. ## Nim code into the input `node` AST.
  237. var index = 0
  238. while index < value.len and
  239. parse_until_symbol(node, value, index): discard
  240. macro tmpli*(body: untyped): untyped =
  241. result = newStmtList()
  242. result.add parseExpr("result = \"\"")
  243. var value = if body.kind in nnkStrLit..nnkTripleStrLit: body.strVal
  244. else: body[1].strVal
  245. parse_template(result, reindent(value))
  246. macro tmpl*(body: untyped): untyped =
  247. result = newStmtList()
  248. var value = if body.kind in nnkStrLit..nnkTripleStrLit: body.strVal
  249. else: body[1].strVal
  250. parse_template(result, reindent(value))
  251. # Run tests
  252. when true:
  253. include otests
  254. echo "Success"