|
| 1 | +# A super fast template engine for cool kids |
| 2 | +# |
| 3 | +# (c) 2025 George Lemon | LGPL-v3 License |
| 4 | +# Made by Humans from OpenPeeps |
| 5 | +# https://github.com/openpeeps/tim | https://openpeeps.dev/packages/tim |
| 6 | + |
| 7 | +import std/[macros, options, os, sequtils, |
| 8 | + strutils, ropes, tables] |
| 9 | + |
| 10 | +import pkg/voodoo/language/[ast, chunk, errors, sym] |
| 11 | + |
| 12 | +type |
| 13 | + GenKind = enum |
| 14 | + gkToplevel |
| 15 | + gkProc |
| 16 | + gkBlockProc |
| 17 | + gkIterator |
| 18 | + |
| 19 | + CodeGen* {.acyclic.} = object |
| 20 | + ## a code generator for a module or proc. |
| 21 | + script: Script # the script all procs go into |
| 22 | + module: Module # the global scope |
| 23 | + chunk: Chunk # the chunk of code we're generating |
| 24 | + case kind: GenKind # what this generator generates |
| 25 | + of gkToplevel: discard |
| 26 | + of gkProc, gkBlockProc: |
| 27 | + procReturnTy: Sym # the proc's return type |
| 28 | + of gkIterator: |
| 29 | + iter: Sym # the symbol representing the iterator |
| 30 | + iterForBody: Node # the for loop's body |
| 31 | + iterForVar: Node # the for loop variable's name |
| 32 | + iterForCtx: Context # the for loop's context |
| 33 | + counter: uint16 |
| 34 | +const |
| 35 | + utilsJS = staticRead(currentSourcePath().parentDir / "tim.js") |
| 36 | + |
| 37 | +proc initCodeGen*(script: Script, module: Module, chunk: Chunk, |
| 38 | + kind = gkToplevel): CodeGen = |
| 39 | + result = CodeGen(script: script, module: module, |
| 40 | + chunk: chunk, kind: kind) |
| 41 | + |
| 42 | +template genGuard(body) = |
| 43 | + ## Wraps ``body`` in a "guard" used for code generation. The guard sets the |
| 44 | + ## line information in the target chunk. This is a helper used by {.codegen.}. |
| 45 | + when declared(node): |
| 46 | + let |
| 47 | + oldFile = gen.chunk.file |
| 48 | + oldLn = gen.chunk.ln |
| 49 | + oldCol = gen.chunk.col |
| 50 | + # gen.chunk.file = node.file |
| 51 | + gen.chunk.ln = node.ln |
| 52 | + gen.chunk.col = node.col |
| 53 | + body |
| 54 | + when declared(node): |
| 55 | + gen.chunk.file = oldFile |
| 56 | + gen.chunk.ln = oldLn |
| 57 | + gen.chunk.col = oldCol |
| 58 | + |
| 59 | +macro codegen(theProc: untyped): untyped = |
| 60 | + ## Wrap ``theProc``'s body in a call to ``genGuard``. |
| 61 | + theProc[3][0] = ident"Rope" |
| 62 | + theProc.params.insert(1, |
| 63 | + newIdentDefs(ident"gen", nnkVarTy.newTree(ident"CodeGen"))) |
| 64 | + if theProc[^1].kind != nnkEmpty: |
| 65 | + let body = nnkStmtList.newTree( |
| 66 | + newAssignment( ident"result", newCall(ident"rope")), |
| 67 | + theProc[^1] |
| 68 | + ) |
| 69 | + theProc[^1] = newCall("genGuard", body) |
| 70 | + result = theProc |
| 71 | + |
| 72 | +# |
| 73 | +# Forward declarations |
| 74 | +# |
| 75 | +proc genStmt(node: Node, indent: int = 0) {.codegen.} |
| 76 | + |
| 77 | +# |
| 78 | +# Lua Transpiler |
| 79 | +# |
| 80 | + |
| 81 | +proc repeatStr(s: string, n: int): string = |
| 82 | + for i in 0..<n: |
| 83 | + result.add(s) |
| 84 | + |
| 85 | +proc getImplValue(node: Node, unquoted = true): string = |
| 86 | + case node.kind |
| 87 | + of nkInt: $node.intVal |
| 88 | + of nkFloat: $node.floatVal |
| 89 | + of nkString: |
| 90 | + if unquoted: "\"" & node.stringVal & "\"" |
| 91 | + else: node.stringVal |
| 92 | + of nkBool: |
| 93 | + if node.boolVal: "true" |
| 94 | + else: "false" |
| 95 | + of nkArray: |
| 96 | + "{" & node.children.mapIt(getImplValue(it)).join(", ") & "}" |
| 97 | + of nkObject: |
| 98 | + "{ " & node.children.mapIt( |
| 99 | + if it.kind == nkIdentDefs: |
| 100 | + let key = it[0].render |
| 101 | + let value = getImplValue(it[^1]) |
| 102 | + key & " = " & value |
| 103 | + else: |
| 104 | + "" |
| 105 | + ).join(", ") & " }" |
| 106 | + else: "" |
| 107 | + |
| 108 | +proc writeVar(node: Node, indent: int = 0): string {.codegen.} = |
| 109 | + let ind = repeatStr(" ", indent) |
| 110 | + for decl in node: |
| 111 | + let varName = decl[0].ident |
| 112 | + let value = decl[^1].getImplValue |
| 113 | + result.add(ind & varName & " = " & value & "\n") |
| 114 | + |
| 115 | +proc renderHandle(node: Node, unquoted = true): string = |
| 116 | + case node.kind |
| 117 | + of nkString: |
| 118 | + if not unquoted: "\"" & node.stringVal & "\"" |
| 119 | + else: node.stringVal |
| 120 | + of nkInt: |
| 121 | + $node.intVal |
| 122 | + of nkFloat: |
| 123 | + $node.floatVal |
| 124 | + of nkBool: |
| 125 | + if node.boolVal: "true" else: "false" |
| 126 | + of nkIdent: |
| 127 | + node.ident |
| 128 | + of nkPrefix: |
| 129 | + node[0].renderHandle & node[1].renderHandle |
| 130 | + of nkPostfix: |
| 131 | + node[0].renderHandle & node[1].renderHandle |
| 132 | + of nkInfix: |
| 133 | + node[1].renderHandle & ' ' & node[0].renderHandle & ' ' & node[2].renderHandle |
| 134 | + of nkCall: |
| 135 | + node[0].renderHandle & '(' & node[1..^1].mapIt(it.renderHandle).join(", ") & ')' |
| 136 | + else: |
| 137 | + "" |
| 138 | + |
| 139 | +proc writeHtml(node: Node, indent: int = 0): string {.codegen.} = |
| 140 | + let tag = node.getTag() |
| 141 | + var |
| 142 | + classNames: seq[string] = @[] |
| 143 | + idVal: string = "" |
| 144 | + customAttrs: seq[(string, string)] = @[] |
| 145 | + for attr in node.attributes: |
| 146 | + if attr.kind == nkHtmlAttribute: |
| 147 | + case attr.attrType |
| 148 | + of htmlAttrClass: |
| 149 | + case attr.attrNode.kind |
| 150 | + of nkString: |
| 151 | + classNames.add(attr.attrNode.stringVal) |
| 152 | + of nkIdent: |
| 153 | + classNames.add(attr.attrNode.ident) |
| 154 | + else: |
| 155 | + discard |
| 156 | + of htmlAttrId: |
| 157 | + case attr.attrNode.kind |
| 158 | + of nkString: |
| 159 | + idVal = attr.attrNode.stringVal |
| 160 | + of nkIdent: |
| 161 | + idVal = attr.attrNode.ident |
| 162 | + else: |
| 163 | + discard |
| 164 | + of htmlAttr: |
| 165 | + if attr.attrNode.kind == nkInfix: |
| 166 | + if attr.attrNode[2].kind == nkInfix: |
| 167 | + if attr.attrNode[2][0].ident == "&": |
| 168 | + let left = attr.attrNode[2][1] |
| 169 | + let right = attr.attrNode[2][2] |
| 170 | + let leftVal = |
| 171 | + if left.kind == nkString: |
| 172 | + left.stringVal |
| 173 | + else: |
| 174 | + "\" .. " & left.renderHandle(false) & " .. \"" |
| 175 | + let rightVal = |
| 176 | + if right.kind == nkIdent and right.ident.len > 0 and right.ident[0] == '$': |
| 177 | + "\" .. "& right.ident[1..^1] & " .. \"" |
| 178 | + else: |
| 179 | + (if right.kind == nkString: right.stringVal |
| 180 | + else: "\" .. " & right.renderHandle(false) & " .. \"") |
| 181 | + customAttrs.add((attr.attrNode[1].renderHandle(true), leftVal & rightVal)) |
| 182 | + else: |
| 183 | + let key = attr.attrNode[1].renderHandle |
| 184 | + let value = |
| 185 | + case attr.attrNode[2].kind |
| 186 | + of nkIdent, nkCall: |
| 187 | + "\" .. " & attr.attrNode[2].renderHandle & " .. \"" |
| 188 | + else: |
| 189 | + attr.attrNode[2].renderHandle |
| 190 | + customAttrs.add((key, value)) |
| 191 | + elif attr.attrNode.kind == nkString: |
| 192 | + customAttrs.add((attr.attrNode.stringVal, "")) |
| 193 | + else: |
| 194 | + discard |
| 195 | + let ind = repeatStr(" ", indent) |
| 196 | + result.add(ind & "html = html .. \"<" & tag) |
| 197 | + if classNames.len > 0: |
| 198 | + result.add(" class=\\\"" & classNames.join(" ") & "\\\"") |
| 199 | + if idVal.len > 0: |
| 200 | + result.add(" id=\\\"" & idVal & "\\\"") |
| 201 | + for (name, value) in customAttrs: |
| 202 | + if value.len > 0: |
| 203 | + result.add(" " & name & "=\\\"" & value & "\\\"") |
| 204 | + else: |
| 205 | + result.add(" " & name) |
| 206 | + result.add(">\"\n") |
| 207 | + for child in node.childElements: |
| 208 | + case child.kind |
| 209 | + of nkBool, nkInt, nkFloat: |
| 210 | + result.add(ind & "html = html .. " & child.renderHandle & "\n") |
| 211 | + of nkString: |
| 212 | + result.add(ind & "html = html .. " & child.renderHandle(false) & "\n") |
| 213 | + of nkCall: |
| 214 | + if child[0].ident[0] == '@': |
| 215 | + discard |
| 216 | + else: |
| 217 | + result.add(gen.genStmt(child, indent + 2)) |
| 218 | + else: |
| 219 | + result.add(gen.genStmt(child, indent + 2)) |
| 220 | + if node.tag notin voidHtmlElements: |
| 221 | + result.add(ind & "html = html .. \"</" & tag & ">\"\n") |
| 222 | + |
| 223 | +proc genStmt(node: Node, indent: int = 0): Rope {.codegen.} = |
| 224 | + result = Rope() |
| 225 | + let ind = repeatStr(" ", indent) |
| 226 | + case node.kind |
| 227 | + of nkVar, nkLet, nkConst: |
| 228 | + result.add(gen.writeVar(node, indent)) |
| 229 | + of nkProc: |
| 230 | + let fnName = node[0].render |
| 231 | + let params = node[2] |
| 232 | + result.add(ind & "function " & fnName & "(") |
| 233 | + result.add(params[1..^1].mapIt(it[0].render).join(", ")) |
| 234 | + result.add(")\n") |
| 235 | + for param in params[1..^1]: |
| 236 | + if param.kind == nkIdentDefs: |
| 237 | + let pname = param[0].render |
| 238 | + result.add(ind & " -- @param " & pname & "\n") |
| 239 | + result.add(gen.genStmt(node[3], indent + 1)) |
| 240 | + result.add(ind & "end\n") |
| 241 | + of nkMacro: |
| 242 | + let fnName = node[0].ident[1..^1] |
| 243 | + let params = node[2] |
| 244 | + result.add(ind & "function " & fnName & "(") |
| 245 | + result.add(params[1..^1].mapIt(it[0].render).join(", ")) |
| 246 | + result.add(")\n") |
| 247 | + for param in params[1..^1]: |
| 248 | + if param.kind == nkIdentDefs: |
| 249 | + let pname = param[0].render |
| 250 | + result.add(ind & " -- @param " & pname & "\n") |
| 251 | + result.add(ind & " -- @return string HTML\n") |
| 252 | + result.add(ind & " local html = ''\n") |
| 253 | + result.add(gen.genStmt(node[3], indent + 1)) |
| 254 | + result.add(ind & " return html\n") |
| 255 | + result.add(ind & "end\n") |
| 256 | + of nkHtmlElement: |
| 257 | + result.add(gen.writeHtml(node, indent)) |
| 258 | + of nkIf: |
| 259 | + result.add(ind & "if " & node[0].render & " then\n") |
| 260 | + result.add(gen.genStmt(node[1], indent + 1)) |
| 261 | + let hasElse = node.children.len mod 2 == 1 |
| 262 | + let elifBranches = if hasElse: node[2..^2] else: node[2..^1] |
| 263 | + for i in countup(0, elifBranches.len - 1, 2): |
| 264 | + result.add(ind & "elseif " & elifBranches[i].render & " then\n") |
| 265 | + result.add(gen.genStmt(elifBranches[i + 1], indent + 1)) |
| 266 | + if hasElse: |
| 267 | + result.add(ind & "else\n") |
| 268 | + result.add(gen.genStmt(node[^1], indent + 1)) |
| 269 | + result.add(ind & "end\n") |
| 270 | + of nkFor: |
| 271 | + let varName = node[0].render |
| 272 | + let iterable = node[1] |
| 273 | + if iterable.kind == nkInfix and iterable[0].kind == nkIdent and iterable[0].ident == "..": |
| 274 | + let startVal = iterable[1].render |
| 275 | + let endVal = iterable[2].render |
| 276 | + result.add(ind & "for " & varName & " = " & startVal & ", " & endVal & " do\n") |
| 277 | + result.add(gen.genStmt(node[2], indent + 1)) |
| 278 | + result.add(ind & "end\n") |
| 279 | + else: |
| 280 | + result.add(ind & "for _, " & varName & " in ipairs(" & iterable.render & ") do\n") |
| 281 | + result.add(gen.genStmt(node[2], indent + 1)) |
| 282 | + result.add(ind & "end\n") |
| 283 | + of nkCall: |
| 284 | + if node[0].ident == "echo": |
| 285 | + result.add(ind & "print(" & node[1..^1].mapIt(it.render).join(", ") & ")\n") |
| 286 | + else: |
| 287 | + result.add(ind & node[0].render & "(" & node[1..^1].mapIt(it.render).join(", ") & ")\n") |
| 288 | + of nkBlock: |
| 289 | + for i, s in node: |
| 290 | + result.add(gen.genStmt(s, indent)) |
| 291 | + of nkReturn: |
| 292 | + if node[0].kind != nkEmpty: |
| 293 | + result.add(ind & "return " & node[0].render & "\n") |
| 294 | + else: |
| 295 | + result.add(ind & "return\n") |
| 296 | + of nkBreak: |
| 297 | + result.add(ind & "break\n") |
| 298 | + of nkContinue: |
| 299 | + result.add(ind & "goto continue\n") |
| 300 | + of nkWhile: |
| 301 | + result.add(ind & "while " & node[0].render & " do\n") |
| 302 | + result.add(gen.genStmt(node[1], indent + 1)) |
| 303 | + result.add(ind & "end\n") |
| 304 | + else: discard |
| 305 | + |
| 306 | +proc genScript*(program: Ast, includePath: Option[string], |
| 307 | + isMainScript: static bool = false, |
| 308 | + isSnippet: static bool = false) {.codegen.} = |
| 309 | + result.add("local $1 = {}\n" % [gen.module.getModuleName()]) |
| 310 | + result.add("\n-- @param ... Additional arguments (not used in this method).\n-- @return string The generated HTML.\n") |
| 311 | + result.add("function $1.render(...)\n" % [gen.module.getModuleName()]) |
| 312 | + result.add(" local html = ''\n") |
| 313 | + for node in program.nodes: |
| 314 | + result.add(gen.genStmt(node, 2)) |
| 315 | + result.add(" return html\n") |
| 316 | + result.add("end\n") |
| 317 | + result.add("return $1\n" % [gen.module.getModuleName()]) |
0 commit comments