Skip to content

Commit

Permalink
WIP JSCompiler to transpile Tim code to JavaScript for client-side re…
Browse files Browse the repository at this point in the history
…ndering

Signed-off-by: George Lemon <[email protected]>
  • Loading branch information
georgelemon committed Feb 14, 2024
1 parent 49d73e4 commit 18f1a72
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 37 deletions.
6 changes: 5 additions & 1 deletion src/tim/engine/ast.nim
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ else:

type
NodeType* = enum
ntUnknown
ntUnknown = "void"

ntLitInt = "int"
ntLitString = "string"
Expand Down Expand Up @@ -46,6 +46,7 @@ type
ntJavaScriptSnippet = "JavaScriptSnippet"
ntYamlSnippet = "YAMLSnippet"
ntJsonSnippet = "JsonSnippet"
ntClientBlock = "ClientSideStatement"

CommandType* = enum
cmdEcho = "echo"
Expand Down Expand Up @@ -156,6 +157,9 @@ type
snippetCode*: string
of ntInclude:
includes*: seq[string]
of ntClientBlock:
clientTargetElement*: string
clientStmt*: seq[Node]
else: discard
meta*: Meta

Expand Down
53 changes: 35 additions & 18 deletions src/tim/engine/compilers/html.nim
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,25 @@ import std/[tables, strutils, json,
jsonutils, options, terminal]

import pkg/jsony
import ../ast, ../logging
import ../ast, ../logging, ./js

from std/xmltree import escape
from ../meta import TimEngine, TimTemplate, TimTemplateType,
getType, getSourcePath

include ./tim # TimCompiler object
import ./tim # TimCompiler object

type
HtmlCompiler* = object of TimCompiler
## Object of a TimCompiler to output `HTML`
when not defined timStandalone:
globalScope: ScopeTable = ScopeTable()
data: JsonNode
jsComp: seq[JSCompiler]

# Forward Declaration
proc evaluateNodes(c: var HtmlCompiler, nodes: seq[Node],
scopetables: var seq[ScopeTable], parentNode: Node = nil): Node {.discardable.}
scopetables: var seq[ScopeTable], parentNodeType: NodeType = ntUnknown): Node {.discardable.}
proc typeCheck(c: var HtmlCompiler, x, node: Node): bool
proc typeCheck(c: var HtmlCompiler, node: Node, expect: NodeType): bool
proc mathInfixEvaluator(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]): Node
Expand Down Expand Up @@ -544,18 +545,18 @@ template evalBranch(branch: Node, body: untyped) =
return # condition is thruty
else: discard

proc evalCondition(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) =
proc evalCondition(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]): Node {.discardable.} =
# Evaluates condition branches
evalBranch node.condIfBranch.expr:
c.evaluateNodes(node.condIfBranch.body, scopetables)
result = c.evaluateNodes(node.condIfBranch.body, scopetables)
if node.condElifBranch.len > 0:
# handle `elif` branches
for elifbranch in node.condElifBranch:
evalBranch elifBranch.expr:
c.evaluateNodes(elifbranch.body, scopetables)
result = c.evaluateNodes(elifbranch.body, scopetables)
if node.condElseBranch.len > 0:
# handle `else` branch
c.evaluateNodes(node.condElseBranch, scopetables)
result = c.evaluateNodes(node.condElseBranch, scopetables)

proc evalConcat(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) =
case node.infixLeft.nt
Expand Down Expand Up @@ -789,14 +790,18 @@ proc getAttrs(c: var HtmlCompiler, attrs: HtmlAttributes, scopetables: var seq[S
of ntAssignableSet:
add attrStr, c.toString(attrNode, scopetables)
of ntIdent:
let x = c.getValue(attrNode, scopetables)
if likely(x != nil):
add attrStr, x.toString()
let xVal = c.getValue(attrNode, scopetables)
if likely(xVal != nil):
add attrStr, xVal.toString()
else: return # undeclaredVariable
of ntCall:
let xVal = c.fnCall(attrNode, scopetables)
if likely(xVal != nil):
add attrStr, xVal.toString()
of ntDotExpr:
let x = c.dotEvaluator(attrNode, scopetables)
if likely(x != nil):
add attrStr, x.toString()
let xVal = c.dotEvaluator(attrNode, scopetables)
if likely(xVal != nil):
add attrStr, xVal.toString()
else: return # undeclaredVariable
else: discard
add result, attrStr.join(" ")
Expand Down Expand Up @@ -848,7 +853,7 @@ template htmlblock(x: Node, body) =
proc htmlElement(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) =
# Handle HTML element
htmlblock node:
c.evaluateNodes(node.nodes, scopetables)
c.evaluateNodes(node.nodes, scopetables, ntHtmlElement)

proc evaluatePartials(c: var HtmlCompiler, includes: seq[string], scopetables: var seq[ScopeTable]) =
# Evaluate included partials
Expand All @@ -857,7 +862,7 @@ proc evaluatePartials(c: var HtmlCompiler, includes: seq[string], scopetables: v
c.evaluateNodes(c.ast.partials[x][0].nodes, scopetables)

proc evaluateNodes(c: var HtmlCompiler, nodes: seq[Node],
scopetables: var seq[ScopeTable], parentNode: Node = nil): Node {.discardable.} =
scopetables: var seq[ScopeTable], parentNodeType: NodeType = ntUnknown): Node {.discardable.} =
# Evaluate a seq[Node] nodes
for i in 0..nodes.high:
case nodes[i].nt
Expand All @@ -884,7 +889,9 @@ proc evaluateNodes(c: var HtmlCompiler, nodes: seq[Node],
of ntAssignExpr:
c.assignExpr(nodes[i], scopetables)
of ntConditionStmt:
c.evalCondition(nodes[i], scopetables)
result = c.evalCondition(nodes[i], scopetables)
if result != nil:
return # a resulted ntCommandStmt Node of type cmdReturn
of ntLoopStmt:
c.evalLoop(nodes[i], scopetables)
of ntLitString, ntLitInt, ntLitFloat, ntLitBool:
Expand All @@ -902,13 +909,23 @@ proc evaluateNodes(c: var HtmlCompiler, nodes: seq[Node],
c.head = c.output
reset(c.output)
of ntCall:
echo c.fnCall(nodes[i], scopetables)
case parentNodeType
of ntHtmlElement:
return c.fnCall(nodes[i], scopetables)
else:
discard c.fnCall(nodes[i], scopetables)
of ntInclude:
c.evaluatePartials(nodes[i].includes, scopetables)
of ntJavaScriptSnippet:
add c.jsOutput, nodes[i].snippetCode
of ntJsonSnippet:
add c.jsonOutput, nodes[i].snippetCode
of ntClientBlock:
var jsCompiler = js.newCompiler(nodes[i].clientStmt, nodes[i].clientTargetElement)
# add c.jsComp, jsCompiler
add c.jsOutput, "document.addEventListener('DOMContentLoaded', function(){"
add c.jsOutput, jsCompiler.getOutput()
add c.jsOutput, "})"
else: discard

#
Expand Down Expand Up @@ -972,7 +989,7 @@ proc getHtml*(c: HtmlCompiler): string =
if c.tplType == ttView and c.jsOutput.len > 0:
add result, "\n" & "<script type=\"text/javascript\">"
add result, c.jsOutput
add result, "\n" & "</script>"
add result, "</script>"

proc getHead*(c: HtmlCompiler): string =
## Returns the top of a split layout
Expand Down
92 changes: 92 additions & 0 deletions src/tim/engine/compilers/js.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# A super fast template engine for cool kids
#
# (c) 2023 George Lemon | LGPL License
# Made by Humans from OpenPeeps
# https://github.com/openpeeps/tim

import std/[tables, strutils, json,
jsonutils, options, terminal]

import pkg/jsony
import ../ast, ../logging

from std/xmltree import escape
from ../meta import TimEngine, TimTemplate, TimTemplateType,
getType, getSourcePath

import ./tim # TimCompiler object

type
JSCompiler* = object of TimCompiler
## Object of a TimCompiler to transpile Tim templates
## to `JavaScript` HtmlElement nodes for client-side
## rendering.
globalScope: ScopeTable = ScopeTable()
data: JsonNode
jsOutputCode: string = "{"
jsCountEl: uint
targetElement: string

const
domCreateElement = "let $1 = document.createElement('$2');"
domSetAttribute = "$1.setAttribute('$2','$3');"
domInsertAdjacentElement = "$1.insertAdjacentElement('beforeend',$2);"
domInnerText = "$1.innerText=\"$2\";"

# Forward Declaration
proc evaluateNodes(c: var JSCompiler, nodes: seq[Node], elp: string = "")


proc toString(c: var JSCompiler, x: Node): string =
result =
case x.nt
of ntLitString:
if x.sVals.len == 0:
x.sVal
else: ""
else: ""

proc getAttrs(c: var JSCompiler, attrs: HtmlAttributes, elx: string): string =
let len = attrs.len
for k, attrNodes in attrs:
var attrStr: seq[string]
for attrNode in attrNodes:
case attrNode.nt
of ntAssignableSet:
add attrStr, c.toString(attrNode)
else: discard # todo
add result, domSetAttribute % [elx, k, attrStr.join(" ")]
# add result, attrStr.join(" ")

proc createHtmlElement(c: var JSCompiler, x: Node, elp: string) =
## Create a new HtmlElement
# c.jsClientSideOutput
let elx = "el" & $(c.jsCountEl)
add c.jsOutputCode, domCreateElement % [elx, x.getTag()]
if x.attrs != nil:
add c.jsOutputCode, c.getAttrs(x.attrs, elx)
inc c.jsCountEl
if x.nodes.len > 0:
c.evaluateNodes(x.nodes, elx)
if elp.len > 0:
add c.jsOutputCode, domInsertAdjacentElement % [elp, elx]
else:
add c.jsOutputCode, domInsertAdjacentElement % ["document.querySelector('" & c.targetElement & "')", elx]

proc evaluateNodes(c: var JSCompiler, nodes: seq[Node], elp: string = "") =
for i in 0..nodes.high:
case nodes[i].nt
of ntHtmlElement:
c.createHtmlElement(nodes[i], elp)
of ntLitString, ntLitInt, ntLitFloat, ntLitBool:
add c.jsOutputCode, domInnerText % [elp, c.toString(nodes[i])]
else: discard # todo

proc newCompiler*(nodes: seq[Node], clientTargetElement: string): JSCompiler =
## Create a new instance of `JSCompiler`
result = JSCompiler(targetElement: clientTargetElement)
result.evaluateNodes(nodes)

proc getOutput*(c: var JSCompiler): string =
add c.jsOutputCode, "}" # end block statement
result = c.jsOutputCode
24 changes: 13 additions & 11 deletions src/tim/engine/compilers/tim.nim
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import ../meta, ../ast, ../logging

type
TimCompiler* = object of RootObj
ast: Ast
tpl: TimTemplate
nl: string = "\n"
output, jsOutput, jsonOutput,
yamlOutput, cssOutput: string
start: bool
case tplType: TimTemplateType
ast*: Ast
tpl*: TimTemplate
nl*: string = "\n"
output*, jsOutput*, jsonOutput*,
yamlOutput*, cssOutput*: string
start*: bool
case tplType*: TimTemplateType
of ttLayout:
head: string
head*: string
else: discard
logger*: Logger
indent: int = 2
minify, hasErrors: bool
stickytail: bool
indent*: int = 2
minify*, hasErrors*: bool
stickytail*: bool
# when `false` inserts a `\n` char
# before closing the HTML element tag.
# Does not apply to `textarea`, `button` and other
Expand Down
39 changes: 32 additions & 7 deletions src/tim/engine/parser.nim
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@ type
lvl: int
prev, curr, next: TokenTuple
engine: TimEngine
tpl: TimTemplate
tpl, tplView: TimTemplate
logger*: Logger
hasErrors*, nilNotError, hasLoadedView: bool
hasErrors*, nilNotError, hasLoadedView,
isMain, refreshAst: bool
parentNode: seq[Node]
includes: Table[string, Meta]
isMain: bool
refreshAst: bool
tplView: TimTemplate
tree: Ast

PrefixFunction = proc(p: var Parser, excludes, includes: set[TokenKind] = {}): Node {.gcsafe.}
Expand Down Expand Up @@ -356,7 +354,6 @@ proc parseAttributes(p: var Parser, attrs: var HtmlAttributes, el: TokenTuple) {
walk p
if p.curr is tkAssign:
walk p
# else: return
if not attrs.hasKey(attrKey.value):
case p.curr.kind
of tkString:
Expand All @@ -373,7 +370,11 @@ proc parseAttributes(p: var Parser, attrs: var HtmlAttributes, el: TokenTuple) {
attrs[attrKey.value] = @[attrValue]
walk p
else:
let x = p.pIdent()
var x: Node
if p.next is tkLP and p.next.wsno == 0:
x = p.pFunctionCall()
else:
x = p.pIdent()
if likely(x != nil):
attrs[attrKey.value] = @[x]
else: errorWithArgs(duplicateAttribute, attrKey, [attrKey.value])
Expand Down Expand Up @@ -646,6 +647,28 @@ prefixHandle pSnippet:
# # result.jsonCode = yaml(p.curr.value).toJsonStr
walk p

prefixHandle pClientSide:
# parse tim template inside a `@client` block
# statement in order to be transpiled to JavaScript
# via engine/compilers/JSCompiler.nim
let this = p.curr
walk p # tkCLient
expect tkIdentifier:
if unlikely(p.curr.value != "target"):
return nil
walk p
result = ast.newNode(ntClientBlock, this)
expectWalk tkAssign
expect tkString:
result.clientTargetElement = p.curr.value
walk p
while p.curr isnot tkEnd and p.curr.isChild(this):
let n: Node = p.getPrefixOrInfix()
if likely(n != nil):
add result.clientStmt, n
else: return nil
expectWalk tkEnd

template handleImplicitDefaultValue {.dirty.} =
# handle implicit default value
walk p
Expand Down Expand Up @@ -807,6 +830,7 @@ proc getPrefixFn(p: var Parser, excludes, includes: set[TokenKind] = {}): Prefix
of tkViewLoader: pViewLoader
of tkSnippetJS: pSnippet
of tkInclude: pInclude
of tkClient: pClientSide
of tkLB: pAnoArray
of tkLC: pAnoObject
of tkFN: pFunction
Expand Down Expand Up @@ -853,6 +877,7 @@ proc parseRoot(p: var Parser, excludes, includes: set[TokenKind] = {}): Node {.g
of tkLB: p.pAnoArray()
of tkLC: p.pAnoObject()
of tkFN: p.pFunction()
of tkClient: p.pClientSide()
else: nil
if unlikely(result == nil):
let tk = if p.curr isnot tkEOF: p.curr else: p.prev
Expand Down
6 changes: 6 additions & 0 deletions src/tim/engine/tokens.nim
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ handlers:
lex.setToken tkInclude, 8
elif lex.next("view"):
lex.setToken tkViewLoader, 5
elif lex.next("client"):
lex.setToken tkClient, 7
elif lex.next("end"):
lex.setToken tkEnd, 4
else: discard

proc handleBackticks(lex: var Lexer, kind: TokenKind) =
Expand Down Expand Up @@ -200,6 +204,8 @@ registerTokens toktokSettings:
snippetYaml
snippetJson
viewLoader
client
`end`
`include`

fn = "fn"
Expand Down

0 comments on commit 18f1a72

Please sign in to comment.