123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- discard """
- output: "Success"
- """
- # Ref:
- # http://nim-lang.org/macros.html
- # http://nim-lang.org/parseutils.html
- # Imports
- import tables, parseutils, macros, strutils
- import annotate
- export annotate
- # Fields
- const identChars = {'a'..'z', 'A'..'Z', '0'..'9', '_'}
- # Procedure Declarations
- proc parse_template(node: NimNode, value: string) {.compiletime.}
- # Procedure Definitions
- proc substring(value: string, index: int, length = -1): string {.compiletime.} =
- ## Returns a string at most `length` characters long, starting at `index`.
- return if length < 0: value.substr(index)
- elif length == 0: ""
- else: value.substr(index, index + length-1)
- proc parse_thru_eol(value: string, index: int): int {.compiletime.} =
- ## Reads until and past the end of the current line, unless
- ## a non-whitespace character is encountered first
- var remainder: string
- var read = value.parseUntil(remainder, {0x0A.char}, index)
- if remainder.skipWhitespace() == read:
- return read + 1
- proc trim_after_eol(value: var string) {.compiletime.} =
- ## Trims any whitespace at end after \n
- var toTrim = 0
- for i in countdown(value.len-1, 0):
- # If \n, return
- if value[i] in [' ', '\t']: inc(toTrim)
- else: break
- if toTrim > 0:
- value = value.substring(0, value.len - toTrim)
- proc trim_eol(value: var string) {.compiletime.} =
- ## Removes everything after the last line if it contains nothing but whitespace
- for i in countdown(value.len - 1, 0):
- # If \n, trim and return
- if value[i] == 0x0A.char:
- value = value.substr(0, i)
- break
- # This is the first character
- if i == 0:
- value = ""
- break
- # Skip change
- if not (value[i] in [' ', '\t']): break
- proc detect_indent(value: string, index: int): int {.compiletime.} =
- ## Detects how indented the line at `index` is.
- # Seek to the beginning of the line.
- var lastChar = index
- for i in countdown(index, 0):
- if value[i] == 0x0A.char:
- # if \n, return the indentation level
- return lastChar - i
- elif not (value[i] in [' ', '\t']):
- # if non-whitespace char, decrement lastChar
- dec(lastChar)
- proc parse_thru_string(value: string, i: var int, strType = '"') {.compiletime.} =
- ## Parses until ending " or ' is reached.
- inc(i)
- if i < value.len-1:
- inc(i, value.skipUntil({'\\', strType}, i))
- proc parse_to_close(value: string, index: int, open='(', close=')', opened=0): int {.compiletime.} =
- ## Reads until all opened braces are closed
- ## ignoring any strings "" or ''
- var remainder = value.substring(index)
- var open_braces = opened
- result = 0
- while result < remainder.len:
- var c = remainder[result]
- if c == open: inc(open_braces)
- elif c == close: dec(open_braces)
- elif c == '"': remainder.parse_thru_string(result)
- elif c == '\'': remainder.parse_thru_string(result, '\'')
- if open_braces == 0: break
- else: inc(result)
- iterator parse_stmt_list(value: string, index: var int): string =
- ## Parses unguided ${..} block
- var read = value.parse_to_close(index, open='{', close='}')
- var expressions = value.substring(index + 1, read - 1).split({ ';', 0x0A.char })
- for expression in expressions:
- let value = expression.strip
- if value.len > 0:
- yield value
- #Increment index & parse thru EOL
- inc(index, read + 1)
- inc(index, value.parse_thru_eol(index))
- iterator parse_compound_statements(value, identifier: string, index: int): string =
- ## Parses through several statements, i.e. if {} elif {} else {}
- ## and returns the initialization of each as an empty statement
- ## i.e. if x == 5 { ... } becomes if x == 5: nil.
- template get_next_ident(expected) =
- var nextIdent: string
- discard value.parseWhile(nextIdent, {'$'} + identChars, i)
- var next: string
- var read: int
- if nextIdent == "case":
- # We have to handle case a bit differently
- read = value.parseUntil(next, '$', i)
- inc(i, read)
- yield next.strip(leading=false) & "\n"
- else:
- read = value.parseUntil(next, '{', i)
- if nextIdent in expected:
- inc(i, read)
- # Parse until closing }, then skip whitespace afterwards
- read = value.parse_to_close(i, open='{', close='}')
- inc(i, read + 1)
- inc(i, value.skipWhitespace(i))
- yield next & ": nil\n"
- else: break
- var i = index
- while true:
- # Check if next statement would be valid, given the identifier
- if identifier in ["if", "when"]:
- get_next_ident([identifier, "$elif", "$else"])
- elif identifier == "case":
- get_next_ident(["case", "$of", "$elif", "$else"])
- elif identifier == "try":
- get_next_ident(["try", "$except", "$finally"])
- proc parse_complex_stmt(value, identifier: string, index: var int): NimNode {.compiletime.} =
- ## Parses if/when/try /elif /else /except /finally statements
- # Build up complex statement string
- var stmtString = newString(0)
- var numStatements = 0
- for statement in value.parse_compound_statements(identifier, index):
- if statement[0] == '$': stmtString.add(statement.substr(1))
- else: stmtString.add(statement)
- inc(numStatements)
- # Parse stmt string
- result = parseExpr(stmtString)
- var resultIndex = 0
- # Fast forward a bit if this is a case statement
- if identifier == "case":
- inc(resultIndex)
- while resultIndex < numStatements:
- # Detect indentation
- let indent = detect_indent(value, index)
- # Parse until an open brace `{`
- var read = value.skipUntil('{', index)
- inc(index, read + 1)
- # Parse through EOL
- inc(index, value.parse_thru_eol(index))
- # Parse through { .. }
- read = value.parse_to_close(index, open='{', close='}', opened=1)
- # Add parsed sub-expression into body
- var body = newStmtList()
- var stmtString = value.substring(index, read)
- trim_after_eol(stmtString)
- stmtString = reindent(stmtString, indent)
- parse_template(body, stmtString)
- inc(index, read + 1)
- # Insert body into result
- var stmtIndex = result[resultIndex].len-1
- result[resultIndex][stmtIndex] = body
- # Parse through EOL again & increment result index
- inc(index, value.parse_thru_eol(index))
- inc(resultIndex)
- proc parse_simple_statement(value: string, index: var int): NimNode {.compiletime.} =
- ## Parses for/while
- # Detect indentation
- let indent = detect_indent(value, index)
- # Parse until an open brace `{`
- var splitValue: string
- var read = value.parseUntil(splitValue, '{', index)
- result = parseExpr(splitValue & ":nil")
- inc(index, read + 1)
- # Parse through EOL
- inc(index, value.parse_thru_eol(index))
- # Parse through { .. }
- read = value.parse_to_close(index, open='{', close='}', opened=1)
- # Add parsed sub-expression into body
- var body = newStmtList()
- var stmtString = value.substring(index, read)
- trim_after_eol(stmtString)
- stmtString = reindent(stmtString, indent)
- parse_template(body, stmtString)
- inc(index, read + 1)
- # Insert body into result
- var stmtIndex = result.len-1
- result[stmtIndex] = body
- # Parse through EOL again
- inc(index, value.parse_thru_eol(index))
- proc parse_until_symbol(node: NimNode, value: string, index: var int): bool {.compiletime.} =
- ## Parses a string until a $ symbol is encountered, if
- ## two $$'s are encountered in a row, a split will happen
- ## removing one of the $'s from the resulting output
- var splitValue: string
- var read = value.parseUntil(splitValue, '$', index)
- var insertionPoint = node.len
- inc(index, read + 1)
- if index < value.len:
- case value[index]
- of '$':
- # Check for duplicate `$`, meaning this is an escaped $
- node.add newCall("add", ident("result"), newStrLitNode("$"))
- inc(index)
- of '(':
- # Check for open `(`, which means parse as simple single-line expression.
- trim_eol(splitValue)
- read = value.parse_to_close(index) + 1
- node.add newCall("add", ident("result"),
- newCall(bindSym"strip", parseExpr("$" & value.substring(index, read)))
- )
- inc(index, read)
- of '{':
- # Check for open `{`, which means open statement list
- trim_eol(splitValue)
- for s in value.parse_stmt_list(index):
- node.add parseExpr(s)
- else:
- # Otherwise parse while valid `identChars` and make expression w/ $
- var identifier: string
- read = value.parseWhile(identifier, identChars, index)
- if identifier in ["for", "while"]:
- ## for/while means open simple statement
- trim_eol(splitValue)
- node.add value.parse_simple_statement(index)
- elif identifier in ["if", "when", "case", "try"]:
- ## if/when/case/try means complex statement
- trim_eol(splitValue)
- node.add value.parse_complex_stmt(identifier, index)
- elif identifier.len > 0:
- ## Treat as simple variable
- node.add newCall("add", ident("result"), newCall("$", ident(identifier)))
- inc(index, read)
- result = true
- # Insert
- if splitValue.len > 0:
- node.insert insertionPoint, newCall("add", ident("result"), newStrLitNode(splitValue))
- proc parse_template(node: NimNode, value: string) =
- ## Parses through entire template, outputing valid
- ## Nim code into the input `node` AST.
- var index = 0
- while index < value.len and
- parse_until_symbol(node, value, index): discard
- macro tmpli*(body: untyped): untyped =
- result = newStmtList()
- result.add parseExpr("result = \"\"")
- var value = if body.kind in nnkStrLit..nnkTripleStrLit: body.strVal
- else: body[1].strVal
- parse_template(result, reindent(value))
- macro tmpl*(body: untyped): untyped =
- result = newStmtList()
- var value = if body.kind in nnkStrLit..nnkTripleStrLit: body.strVal
- else: body[1].strVal
- parse_template(result, reindent(value))
- # Run tests
- when true:
- include otests
- echo "Success"
|