itsumura-h / nim-basolato

An asynchronous fullstack web framework for Nim.
MIT License
236 stars 18 forks source link

new template engine #294

Closed itsumura-h closed 4 months ago

itsumura-h commented 4 months ago
import std/json
import std/macros
import std/strutils
import std/strformat
import std/tables
import ./libs/random_string

# ==================== xmlEncode ====================
# extract from `cgi` to be able to run for JavaScript.
# https://nim-lang.org/docs/cgi.html#xmlEncode%2Cstring

proc addXmlChar(dest: var string, c: char) {.inline.} =
  case c
  of '&': add(dest, "&")
  of '<': add(dest, "&lt;")
  of '>': add(dest, "&gt;")
  of '\"': add(dest, "&quot;")
  else: add(dest, c)

proc xmlEncode*(s: string): string =
  ## Encodes a value to be XML safe:
  ## * `"` is replaced by `&quot;`
  ## * `<` is replaced by `&lt;`
  ## * `>` is replaced by `&gt;`
  ## * `&` is replaced by `&amp;`
  ## * every other character is carried over.
  result = newStringOfCap(s.len + s.len shr 2)
  for i in 0..len(s)-1: addXmlChar(result, s[i])

# ==================== libView ====================
proc toString*(val:JsonNode):string =
  case val.kind
  of JString:
    return val.getStr.xmlEncode
  of JInt:
    return $(val.getInt)
  of JFloat:
    return $(val.getFloat)
  of JBool:
    return $(val.getBool)
  of JNull:
    return ""
  else:
    raise newException(JsonKindError, "val is array")

proc toString*(val:string):string =
  return val.strip.xmlEncode

proc toString*(val:bool | int | float):string =
  return val.`$`.xmlEncode

# ==================== Component ====================
type Component* = ref object
  value:string
  id:string

proc new*(_:type Component):Component =
  let id = randStr(10)
  return Component(value:"", id:id)

proc add*(self:Component, value:string) =
  self.value.add(value)

proc toString*(self:Component):string =
  return self.value.strip()

proc `$`*(self:Component):string =
  return self.toString()

proc id*(self:Component):string = self.id

# ==================== parse template ====================
type BlockType = enum
  strBlock
  ifBlock               # $if
  elifBlock             # $elif
  elseBlock             # $else
  forBlock              # $for
  caseBlock             # $case
  ofBlock               # $of
  whileBlock            # $while
  displayVariableBlock  # $()
  nimCodeBlock          # ${}

proc identifyBlockType(str:string, point:int):BlockType =
  if str.substr(point, point+2) == "$if":
    return ifBlock
  elif str.substr(point, point+4) == "$elif":
    return elifBlock
  elif str.substr(point, point+4) == "$else":
    return elseBlock
  elif str.substr(point, point+3) == "$for":
    return forBlock
  elif str.substr(point, point+4) == "$case":
    return caseBlock
  elif str.substr(point, point+2) == "$of":
    return ofBlock
  elif str.substr(point, point+5) == "$while":
    return whileBlock
  elif str.substr(point, point+1) == "$(":
    return displayVariableBlock
  elif str.substr(point, point+1) == "${":
    return nimCodeBlock
  else:
    return strBlock

proc findStrBlock(str:string, point:int):(int, string) =
  var count = -1
  var isDoller = false
  for s in str[point..^1]:
    count += 1
    if s == '$':
      isDoller = true
      break
    elif s == '}':
      break

  let resPoint = point + count
  let resStr = str.substr(point, resPoint-1) # $、}を含めない

  if isDoller:
    return (resPoint, resStr) # $からスタート
  else:
    return (resPoint+1, resStr) # 「}」の次からスタート

proc findNimVariableBlock(str:string, point:int):(int, string) =
  let point = point + 2 # 「$(」の分進める
  var count = -1
  for s in str[point..^1]:
    count += 1
    if s == ')':
      break

  let resPoint = point + count
  let resStr = str.substr(point, resPoint-1) # 「)」を含めない
  return (resPoint+1, resStr) # 「)」の次からスタート

proc findNimBlock(str:string, point:var int):(int, string) =
  let point = point + 1 # 「$」の分進める
  var count = -1
  for s in str[point..^1]:
    count += 1
    if s == '{':
      break

  let resPoint = point + count
  let resStr = str.substr(point, resPoint-1) # 「{」を含めない
  return (resPoint+1, resStr) # 「{」の次からスタート

proc findNimCodeBlock(str:string, point:var int):(int, string) =
  let point = point + 2 # 「${」の分進める
  var count = -1
  for s in str[point..^1]:
    count += 1
    if s == '}':
      break

  let resPoint = point + count
  let resStr = str.substr(point, resPoint-1) # 「}」を含めない
  return (resPoint+1, resStr) # 「}」の次からスタート

proc reindent(str:string, indentLevel:int):string  =
  let indent = "  ".repeat(indentLevel)
  return indent & str

macro tmpl*(html: untyped): untyped =
  var body = "result = Component.new()\n"
  var point = 0
  var indentLevel = 0
  var blockType = strBlock
  while true:
    if point == html.repr.len:
      break

    blockType = identifyBlockType(html.repr, point)

    case blockType
    of strBlock:
      var (resPoint, resStr) = findStrBlock(html.repr, point)
      resStr = resStr.strip().replace("\n", "") # 改行コードを消す
      resStr = &"result.add({resStr.repr})\n"
      resStr = reindent(resStr, indentLevel)
      body.add(resStr)
      point = resPoint
      # resPointの1つ前が「}」の場合、indentLevelを下げる
      if html.repr[resPoint-1] == '}':
        indentLevel -= 1
    of ifBlock:
      var (resPoint, resStr) = findNimBlock(html.repr, point)
      resStr = resStr.strip() & ":\n"
      resStr = reindent(resStr, indentLevel)
      body.add(resStr)
      indentLevel += 1
      point = resPoint
    of elifBlock:
      var (resPoint, resStr) = findNimBlock(html.repr, point)
      resStr = resStr.strip() & ":\n"
      resStr = reindent(resStr, indentLevel)
      body.add(resStr)
      indentLevel += 1
      point = resPoint
    of elseBlock:
      var (resPoint, resStr) = findNimBlock(html.repr, point)
      resStr = resStr.strip() & ":\n"
      resStr = reindent(resStr, indentLevel)
      body.add(resStr)
      indentLevel += 1
      point = resPoint
    of forBlock:
      var (resPoint, resStr) = findNimBlock(html.repr, point)
      resStr = resStr.strip() & ":\n"
      resStr = reindent(resStr, indentLevel)
      body.add(resStr)
      indentLevel += 1
      point = resPoint
    of caseBlock:
      var (resPoint, resStr) = findNimBlock(html.repr, point)
      resStr = resStr.strip() & ":\n"
      resStr = reindent(resStr, indentLevel)
      point = resPoint
      body.add(resStr)
    of ofBlock:
      var (resPoint, resStr) = findNimBlock(html.repr, point)
      resStr = resStr.strip() & ":\n"
      resStr = reindent(resStr, indentLevel)
      point = resPoint
      body.add(resStr)
      indentLevel += 1
    of whileBlock:
      var (resPoint, resStr) = findNimBlock(html.repr, point)
      resStr = resStr.strip() & ":\n"
      resStr = reindent(resStr, indentLevel)
      point = resPoint
      body.add(resStr)
      indentLevel += 1
    of displayVariableBlock:
      var (resPoint, resStr) = findNimVariableBlock(html.repr, point)
      resStr = resStr.strip()
      resStr = &"result.add(toString(({resStr})))"
      resStr = reindent(resStr, indentLevel)
      body.add(resStr & "\n")
      point = resPoint
    of nimCodeBlock:
      var (resPoint, resStr) = findNimCodeBlock(html.repr, point)
      resStr = resStr.strip() & "\n"
      resStr = reindent(resStr, indentLevel)
      body.add(resStr)
      point = resPoint

    if point == html.repr.len:
      break

  return body.parseStmt()