diff --git a/src/tim/engine/compilers/html.nim b/src/tim/engine/compilers/html.nim index d70ebe4..830ba67 100755 --- a/src/tim/engine/compilers/html.nim +++ b/src/tim/engine/compilers/html.nim @@ -88,6 +88,7 @@ const domSetAttribute = "$1.setAttribute('$2','$3');" domInsertAdjacentElement = "$1.insertAdjacentElement('beforeend',$2);" domInnerText = "$1.innerText=\"$2\";" + stdlibPaths = ["std/system", "std/strings", "std/arrays", "std/os"] when not defined timStandalone: # Scope API, available for library version of TimEngine @@ -109,12 +110,12 @@ when not defined timStandalone: else: c.globalScope[node.varName] = node of ntFunction: - if c.ast.src != "std/system": + if c.ast.src notin stdlibPaths: if scopetables.len > 0: scopetables[^1][node.fnIdent] = node - else: - c.globalScope[node.fnIdent] = node - else: discard # todo + else: c.globalScope[node.fnIdent] = node + else: + c.globalScope[node.fnIdent] = node else: discard proc getCurrentScope(c: var HtmlCompiler, @@ -380,6 +381,34 @@ proc walkAccessorStorage(c: var HtmlCompiler, let lhs = c.bracketEvaluator(lhs, scopetables) if likely(lhs != nil): return c.walkAccessorStorage(lhs, rhs, scopetables) + of ntLitString: + case rhs.nt + of ntLitInt: + try: + return ast.newString($(lhs.sVal[rhs.iVal])) + except Defect: + compileErrorWithArgs(indexDefect, lhs.meta, + [$(rhs.iVal), "0.." & $(lhs.arrayItems.high)]) + of ntIndexRange: + let + l = c.getValue(rhs.rangeNodes[0], scopetables) + r = c.getValue(rhs.rangeNodes[1], scopetables) + if likely(l != nil and r != nil): + let l = l.iVal + let r = r.iVal + try: + case rhs.rangeLastIndex + of false: + result = ast.newString($(lhs.sVal[l..r])) + of true: + result = ast.newString($(lhs.sVal[l..^r])) + except Defect: + let someRange = + if rhs.rangeLastIndex: $(l) & "..^" & $(r) + else: $(l) & ".." & $(r) + compileErrorWithArgs(indexDefect, lhs.meta, + [someRange, "0.." & $(lhs.sVal.high)]) + else: discard of ntLitArray: case rhs.nt of ntLitInt: @@ -409,7 +438,19 @@ proc walkAccessorStorage(c: var HtmlCompiler, compileErrorWithArgs(indexDefect, lhs.meta, [someRange, "0.." & $(lhs.arrayItems.high)]) else: discard # todo error? - else: compileErrorWithArgs(invalidAccessorStorage, + else: + case rhs.nt + of ntIdent: + let some = c.getScope(rhs.identName, scopetables) + if likely(some.scopeTable != nil): + case some.scopeTable[rhs.identName].nt + of ntFunction: + # evaluate a function call and return the result + # if the retun type is not void, otherwise nil + return c.unsafeCall(lhs, some.scopeTable[rhs.identName], scopetables) + else: discard + else: discard + compileErrorWithArgs(invalidAccessorStorage, rhs.meta, [rhs.toString, $lhs.nt]) else: discard @@ -470,6 +511,9 @@ proc infixEvaluator(c: var HtmlCompiler, lhs, rhs: Node, infixOp: InfixOp, scopetables: var seq[ScopeTable]): bool = # Evaluates comparison expressions if unlikely(lhs == nil or rhs == nil): return + let lhs = c.getValue(lhs, scopetables) + let rhs = c.getValue(rhs, scopetables) + if unlikely(lhs == nil or rhs == nil): return case infixOp: of EQ: case lhs.nt: @@ -662,7 +706,6 @@ proc getValue(c: var HtmlCompiler, node: Node, # evaluates an identifier let some = c.getScope(node.identName, scopetables) if likely(some.scopeTable != nil): - # echo some.scopeTable[node.identName].nt case some.scopeTable[node.identName].nt of ntFunction: # evaluate a function call and return the result @@ -675,6 +718,8 @@ proc getValue(c: var HtmlCompiler, node: Node, return c.data["local"].toTimNode if node.identName == "app": return c.data["global"].toTimNode + if node.identArgs.len > 0: + compileErrorWithArgs(fnUndeclared, [node.identName]) compileErrorWithArgs(undeclaredVariable, [node.identName]) of ntAssignableSet, ntIndexRange: # return literal nodes @@ -1380,10 +1425,12 @@ proc walkNodes(c: var HtmlCompiler, nodes: seq[Node], of ntIdent: # case parentNodeType # of ntHtmlElement: - # let returnNode = c.fnCall(node, scopetables) - # if likely(returnNode != nil): - # write returnNode, true, false + # echo node + # let returnNode = c.fnCall(node, scopetables) + # if likely(returnNode != nil): + # write returnNode, true, false # else: + # discard # let x = c.fnCall(node, scopetables) # if unlikely x != nil: # if parentNodeType != ntFunction and x.nt != ntHtmlElement: diff --git a/src/tim/engine/parser.nim b/src/tim/engine/parser.nim index 455034d..7a1691b 100755 --- a/src/tim/engine/parser.nim +++ b/src/tim/engine/parser.nim @@ -89,6 +89,9 @@ proc parseMathExp(p: var Parser, lhs: Node): Node {.gcsafe.} proc parseCompExp(p: var Parser, lhs: Node): Node {.gcsafe.} proc parseTernaryExpr(p: var Parser, lhs: Node): Node {.gcsafe.} +proc parseModule(engine: TimEngine, moduleName: string, + code: SourceCode = SourceCode("")): Ast {.gcsafe.} + template caseNotNil(x: Node, body): untyped = if likely(x != nil): body @@ -286,7 +289,9 @@ proc parseDotExpr(p: var Parser, lhs: Node): Node {.gcsafe.} = result.lhs = lhs walk p # tkDot if p.isFnCall(): - result.rhs = p.pFunctionCall() + let fnCallNode = p.pFunctionCall() + caseNotNil fnCallNode: + result.rhs = fnCallNode elif p.curr is tkIdentifier: result.rhs = ast.newIdent(p.curr) walk p @@ -841,12 +846,16 @@ prefixHandle pInclude: prefixHandle pImport: # parse `@import` - if likely p.next is tkString: - let tk = p.curr - walk p - result = ast.newNode(ntImport, tk) - add result.modules, p.curr.value - walk p + {.gcsafe.}: + if likely p.next is tkString: + let tk = p.curr + walk p + result = ast.newNode(ntImport, tk) + add result.modules, p.curr.value + p.tree.modules[p.curr.value] = + p.engine.parseModule(p.curr.value, std(p.curr.value)[1]) + p.tree.modules[p.curr.value].src = p.curr.value + walk p prefixHandle pSnippet: case p.curr.kind @@ -973,6 +982,10 @@ prefixHandle pFunctionCall: walk p, 2 # we know tkLP is next so we'll skip it while p.curr isnot tkRP: let argNode = p.getPrefixOrInfix(includes = tkAssignableSet) + if p.curr is tkComma and p.next in tkAssignableSet: + walk p + elif p.curr isnot tkRP: + return nil caseNotNil argNode: add result.identArgs, argNode walk p # tkRP @@ -1212,27 +1225,28 @@ template collectImporterErrors = p.handle.hasErrors = true proc parseModule(engine: TimEngine, moduleName: string, - code: SourceCode = SourceCode("")): Ast = - var p = Parser( - tree: Ast(src: moduleName), - engine: engine, - lex: newLexer(code.string, allowMultilineStrings = true), - logger: Logger(filePath: "") - ) - p.curr = p.lex.getToken() - p.next = p.lex.getToken() - # p.skipComments() # if any - while p.curr isnot tkEOF: - if unlikely(p.lex.hasError): - p.logger.newError(internalError, p.curr.line, - p.curr.col, false, p.lex.getError) - if unlikely(p.hasErrors): - break - let node = p.parseRoot() - if node != nil: - add p.tree.nodes, node - p.lex.close() - result = p.tree + code: SourceCode = SourceCode("")): Ast {.gcsafe.} = + {.gcsafe.}: + var p = Parser( + tree: Ast(src: moduleName), + engine: engine, + lex: newLexer(code.string, allowMultilineStrings = true), + logger: Logger(filePath: "") + ) + p.curr = p.lex.getToken() + p.next = p.lex.getToken() + # p.skipComments() # if any + while p.curr isnot tkEOF: + if unlikely(p.lex.hasError): + p.logger.newError(internalError, p.curr.line, + p.curr.col, false, p.lex.getError) + if unlikely(p.hasErrors): + break + let node = p.parseRoot() + if node != nil: + add p.tree.nodes, node + p.lex.close() + result = p.tree proc initSystemModule(p: var Parser) = ## Make `std/system` available by default diff --git a/src/tim/engine/stdlib.nim b/src/tim/engine/stdlib.nim new file mode 100644 index 0000000..fa95c6c --- /dev/null +++ b/src/tim/engine/stdlib.nim @@ -0,0 +1,405 @@ +# A super fast stylesheet language for cool kids +# +# (c) 2023 George Lemon | LGPL License +# Made by Humans from OpenPeeps +# https://github.com/openpeeps/bro + +import std/[macros, enumutils, critbits] +import ./ast + +# std lib dependencies +import pkg/[jsony, nyml] +import std/[os, math, fenv, strutils, sequtils, + random, unicode, json, base64] + +# import ./css + +type + Arg* = tuple[name: string, value: Node] + NimCall* = proc(args: openarray[Arg], returnType: NodeType = ntUnknown): Node + + Module = CritBitTree[NimCall] + SourceCode* = distinct string + + Stdlib* = CritBitTree[(Module, SourceCode)] + + StringsModule* = object of CatchableError + ArraysModule* = object of CatchableError + OSModule* = object of CatchableError + ColorsModule* = object of CatchableError + SystemModule* = object of CatchableError + +var + stdlib*: Stdlib + strutilsModule {.threadvar.}, + sequtilsModule {.threadvar.}, + osModule {.threadvar.}, + critbitsModule {.threadvar.}, + systemModule {.threadvar.}, + mathModule {.threadvar.}, + chromaModule {.threadvar.}: Module + +proc toNimSeq*(node: Node): seq[string] = + for item in node.arrayItems: + result.add(item.sVal) + +macro initStandardLibrary() = + type + Wrapper = proc(args: seq[Node]): Node {.nimcall.} + + FwdType = enum + fwdProc + fwdIterator + + Forward = object + fwdType: FwdType + id: string + # function identifier (nim side) + alias: string + # if not provided, it will use the `id` + # for the bass function name + returns: NodeType + # the return type, one of: `ntLitString`, `ntLitInt`, + # `ntLitBool`, `ntLitFloat`, `ntLitArray`, `ntLitObject` + args: seq[(NodeType, string)] + # a seq of NodeType for type matching + wrapper: NimNode + # wraps nim function + hasWrapper: bool + loadFrom: string + + proc addFunction(id: string, args: openarray[(NodeType, string)], nt: NodeType): string = + var p = args.map do: + proc(x: (NodeType, string)): string = + "$1: $2" % [x[1], $(x[0])] + result = "fn $1*($2): $3\n" % [id, p.join(", "), $nt] + + proc fwd(id: string, returns: NodeType, args: openarray[(NodeType, string)] = [], + alias = "", wrapper: NimNode = nil, loadFrom = ""): Forward = + Forward(id: id, returns: returns, args: args.toSeq, + alias: alias, wrapper: wrapper, hasWrapper: wrapper != nil, + loadFrom: loadFrom) + + # proc `*`(nt: NodeType, count: int): seq[NodeType] = + # for i in countup(1, count): + # result.add(nt) + + proc argToSeq[T](arg: Arg): T = + toNimSeq(arg.value) + + template formatWrapper: untyped = + try: + ast.newString(format(args[0].value.sVal, argToSeq[seq[string]](args[1]))) + except ValueError as e: + raise newException(StringsModule, e.msg) + + template systemStreamFunction: untyped = + try: + let filepath = + if not isAbsolute(args[0].value.sVal): + absolutePath(args[0].value.sVal) + else: args[0].value.sVal + let str = readFile(filepath) + let ext = filepath.splitFile.ext + if ext == ".json": + return ast.newStream(str.fromJson(JsonNode)) + elif ext in [".yml", ".yaml"]: + return ast.newStream(yaml(str).toJson.get) + else: + echo "error" + except IOError as e: + raise newException(SystemModule, e.msg) + except JsonParsingError as e: + raise newException(SystemModule, e.msg) + + template systemRandomize: untyped = + randomize() + ast.newInteger(rand(args[0].value.iVal)) + + template systemInc: untyped = + inc args[0].value.iVal + echo args[0].value + args[0].value + + template convertToString: untyped = + var str: ast.Node + var val = args[0].value + case val.nt: + of ntLitInt: + str = ast.newString($(val.iVal)) + of ntLitFloat: + str = ast.newString($(val.fVal)) + else: discard + str + + let + fnSystem = @[ + # fwd("json", ntStream, [(ntLitString, "path")], wrapper = getAst(systemStreamFunction())), + # fwd("yaml", ntStream, [(ntLitString, "path")], wrapper = getAst(systemStreamFunction())), + fwd("rand", ntLitInt, [(ntLitInt, "max")], "random", wrapper = getAst(systemRandomize())), + fwd("len", ntLitInt, [(ntLitString, "x")]), + # fwd("len", ntLitInt, [(ntLitArray, "x")]), + fwd("encode", ntLitString, [(ntLitString, "x")], loadFrom = "base64"), + fwd("decode", ntLitString, [(ntLitString, "x")], loadFrom = "base64"), + fwd("toString", ntLitString, [(ntLitInt, "x")], wrapper = getAst(convertToString())) + # fwd("int", ntInt, [(ntLitInt, "x")], "increment", wrapper = getAst(systemInc())), + ] + + let + fnMath = @[ + fwd("ceil", ntLitFloat, [(ntLitFloat, "x")]), + # fwd("clamp") need to add support for ranges + fwd("floor", ntLitFloat, [(ntLitFloat, "x")]), + fwd("max", ntLitInt, [(ntLitInt, "x"), (ntLitInt, "y")], loadFrom = "system"), + fwd("min", ntLitInt, [(ntLitInt, "x"), (ntLitInt, "y")], loadFrom = "system"), + fwd("round", ntLitFloat, [(ntLitFloat, "x")]), + # fwd("abs", ntLitInt, [(ntLitInt, "x")]), + fwd("hypot", ntLitFloat, [(ntLitFloat, "x"), (ntLitFloat, "y")]), + fwd("log", ntLitFloat, [(ntLitFloat, "x"), (ntLitFloat, "base")]), + fwd("pow", ntLitFloat, [(ntLitFloat, "x"), (ntLitFloat, "y")]), + fwd("sqrt", ntLitFloat, [(ntLitFloat, "x")]), + fwd("cos", ntLitFloat, [(ntLitFloat, "x")]), + fwd("sin", ntLitFloat, [(ntLitFloat, "x")]), + fwd("tan", ntLitFloat, [(ntLitFloat, "x")]), + fwd("arccos", ntLitFloat, [(ntLitFloat, "x")], "acos"), + fwd("arcsin", ntLitFloat, [(ntLitFloat, "x")], "asin"), + fwd("radToDeg", ntLitFloat, [(ntLitFloat, "d")], "rad2deg"), + fwd("degToRad", ntLitFloat, [(ntLitFloat, "d")], "deg2rad"), + fwd("arctan", ntLitFloat, [(ntLitFloat, "x")], "atan"), + fwd("arctan2", ntLitFloat, [(ntLitFloat, "x"), (ntLitFloat, "y")], "atan2"), + fwd("trunc", ntLitFloat, [(ntLitFloat, "x")]), + ] + # std/strings + # implements common functions for working with strings + # https://nim-lang.github.io/Nim/strutils.html + let + fnStrings = @[ + fwd("endsWith", ntLitBool, [(ntLitString, "s"), (ntLitString, "suffix")]), + fwd("startsWith", ntLitBool, [(ntLitString, "s"), (ntLitString, "prefix")]), + fwd("capitalizeAscii", ntLitString, [(ntLitString, "s")], "capitalize"), + fwd("replace", ntLitString, [(ntLitString, "s"), (ntLitString, "sub"), (ntLitString, "by")]), + fwd("toLowerAscii", ntLitString, [(ntLitString, "s")], "toLower"), + fwd("contains", ntLitBool, [(ntLitString, "s"), (ntLitString, "sub")]), + fwd("parseBool", ntLitBool, [(ntLitString, "s")]), + fwd("parseInt", ntLitInt, [(ntLitString, "s")]), + fwd("parseFloat", ntLitFloat, [(ntLitString, "s")], "toFloat"), + fwd("format", ntLitString, [(ntLitString, "s"), (ntLitArray, "a")], wrapper = getAst(formatWrapper())) + ] + + # std/arrays + # implements common functions for working with arrays (sequences) + # https://nim-lang.github.io/Nim/sequtils.html + + template arraysContains: untyped = + ast.newBool(system.contains(toNimSeq(args[0].value), args[1].value.sVal)) + + template arraysAdd: untyped = + add(args[0].value.arrayItems, args[1].value) + + template arraysShift: untyped = + try: + delete(args[0].value.arrayItems, 0) + except IndexDefect as e: + raise newException(ArraysModule, e.msg) + + template arraysPop: untyped = + try: + delete(args[0].value.arrayItems, args[0].value.arrayItems.high) + except IndexDefect as e: + raise newException(ArraysModule, e.msg) + + template arraysShuffle: untyped = + randomize() + shuffle(args[0].value.arrayItems) + + template arraysJoin: untyped = + ast.newString(strutils.join(toNimSeq(args[0].value), args[1].value.sVal)) + + template arraysDelete: untyped = + delete(args[0].value.arrayItems, args[1].value.iVal) + + template arraysFind: untyped = + for i in 0..args[0].value.arrayItems.high: + if args[0].value.arrayItems[i].sVal == args[1].value.sVal: + return ast.newInteger(i) + + let + fnArrays = @[ + fwd("contains", ntLitBool, [(ntLitArray, "x"), (ntLitString, "item")], wrapper = getAst arraysContains()), + fwd("add", ntUnknown, [(ntLitArray, "x"), (ntLitString, "item")], wrapper = getAst arraysAdd()), + fwd("shift", ntUnknown, [(ntLitArray, "x")], wrapper = getAst arraysShift()), + fwd("pop", ntUnknown, [(ntLitArray, "x")], wrapper = getAst arraysPop()), + fwd("shuffle", ntUnknown, [(ntLitArray, "x")], wrapper = getAst arraysShuffle()), + fwd("join", ntLitString, [(ntLitArray, "x"), (ntLitString, "sep")], wrapper = getAst arraysJoin()), + fwd("delete", ntUnknown, [(ntLitArray, "x"), (ntLitInt, "pos")], wrapper = getAst arraysDelete()), + fwd("find", ntLitInt, [(ntLitArray, "x"), (ntLitString, "item")], wrapper = getAst arraysFind()), + ] + # fnObjects = @[ + # fwd("hasKey", ntLitBool, [ntLitObject, ntLitString]), + # # fwd("keys", ntLitArray, [ntLitObject]) + # ] + + + # std/os + # implements some read-only basic operating system functions + # https://nim-lang.org/docs/os.html + template osWalkFiles: untyped = + let x = toSeq(walkPattern(args[0].value.sVal)) + var a = ast.newArray() + a.arrayType = ntLitString + a.arrayItems = + x.map do: + proc(xpath: string): Node = ast.newString(xpath) + a + let + fnOs = @[ + fwd("absolutePath", ntLitString, [(ntLitString, "path")]), + fwd("dirExists", ntLitBool, [(ntLitString, "path")]), + fwd("fileExists", ntLitBool, [(ntLitString, "path")]), + fwd("normalizedPath", ntLitString, [(ntLitString, "path")], "normalize"), + # fwd("splitFile", ntTuple, [ntLitString]), + fwd("extractFilename", ntLitString, [(ntLitString, "path")], "getFilename"), + fwd("isAbsolute", ntLitBool, [(ntLitString, "path")]), + fwd("readFile", ntLitString, [(ntLitString, "path")], loadFrom="system"), + fwd("isRelativeTo", ntLitBool, [(ntLitString, "path"), (ntLitString, "base")], "isRelative"), + fwd("getCurrentDir", ntLitString), + fwd("joinPath", ntLitString, [(ntLitString, "head"), (ntLitString, "tail")], "join"), + fwd("parentDir", ntLitString, [(ntLitString, "path")]), + fwd("walkFiles", ntLitArray, [(ntLitString, "path")], wrapper = getAst osWalkFiles()), + ] + + result = newStmtList() + let libs = [ + ("system", fnSystem, "system"), + ("math", fnMath, "math"), + ("strutils", fnStrings, "strings"), + ("sequtils", fnArrays, "arrays"), + ("os", fnOs, "os") + ] + echo "Generate Standard Library" + for lib in libs: + var sourceCode: string + for fn in lib[1]: + var + lambda = nnkLambda.newTree(newEmptyNode(), newEmptyNode(), newEmptyNode()) + params = newNimNode(nnkFormalParams) + params.add( + ident("Node"), + nnkIdentDefs.newTree( + ident("args"), + nnkBracketExpr.newTree( + ident("openarray"), + ident("Arg") + ), + newEmptyNode() + ), + nnkIdentDefs.newTree( + ident("returnType"), + ident("NodeType"), + ident(symbolName(fn.returns)) + ) + ) + lambda.add(params) + lambda.add(newEmptyNode()) + lambda.add(newEmptyNode()) + var valNode = + case fn.returns: + of ntLitBool: "newBool" + of ntLitString: "newString" + of ntLitInt: "newInteger" + of ntLitFloat: "newFloat" + of ntLitArray: "newArray" # todo implement toArray + of ntLitObject: "newObject" # todo implement toObject + else: "" + var i = 0 + var fnIdent = if fn.alias.len != 0: fn.alias else: fn.id + add sourceCode, addFunction(fnIdent, fn.args, fn.returns) + var callNode: NimNode + if not fn.hasWrapper: + var callableNode = + if lib[0] != "system": + if fn.loadFrom.len == 0: + newCall(newDotExpr(ident(lib[0]), ident(fn.id))) + else: + newCall(newDotExpr(ident(fn.loadFrom), ident(fn.id))) + else: + if fn.loadFrom.len == 0: + newCall(newDotExpr(ident("system"), ident(fn.id))) + else: + newCall(newDotExpr(ident(fn.loadFrom), ident(fn.id))) + for arg in fn.args: + let fieldName = + case arg[0] + of ntLitBool: "bVal" + of ntLitString: "sVal" + of ntLitInt: "iVal" + of ntLitFloat: "fVal" + of ntLitArray: "arrayItems" + of ntLitObject: "pairsVal" + else: "None" + if fieldName.len != 0: + callableNode.add( + newDotExpr( + newDotExpr( + nnkBracketExpr.newTree( + ident("args"), + newLit(i) + ), + ident("value") + ), + ident(fieldName) + ) + ) + else: + callableNode.add( + newDotExpr( + nnkBracketExpr.newTree( + ident("args"), + newLit(i) + ), + ident("value") + ), + ) + inc i + callNode = newCall(ident(valNode), callableNode) + else: + callNode = fn.wrapper + lambda.add(newStmtList(callNode)) + add result, + newAssignment( + nnkBracketExpr.newTree( + ident(lib[0] & "Module"), + newLit(fnIdent) + ), + lambda + ) + add result, + newAssignment( + nnkBracketExpr.newTree( + ident("stdlib"), + newLit("std/" & lib[2]) + ), + nnkTupleConstr.newTree( + ident(lib[0] & "Module"), + newCall(ident("SourceCode"), newLit(sourceCode)) + ) + ) + when not defined release: + echo "std/" & lib[2] + echo sourceCode + +proc initstdlib*() = + {.gcsafe.}: + initStandardLibrary() + +proc exists*(lib: string): bool = + ## Checks if if `lib` exists in `Stdlib` + result = stdlib.hasKey(lib) + +proc std*(lib: string): (Module, SourceCode) {.raises: KeyError.} = + ## Retrieves a module from `Stdlib` + result = stdlib[lib] + +proc call*(lib, fnName: string, args: seq[Arg]): Node = + ## Retrieves a Nim proc from `module` + result = stdlib[lib][0][fnName](args) diff --git a/src/tim/engine/tokens.nim b/src/tim/engine/tokens.nim index 1973b41..94279ea 100755 --- a/src/tim/engine/tokens.nim +++ b/src/tim/engine/tokens.nim @@ -101,6 +101,8 @@ handlers: lex.token = "@placeholder" elif lex.next("include"): lex.setToken tkInclude, 8 + elif lex.next("import"): + lex.setToken tkImport, 7 elif lex.next("view"): lex.setToken tkViewLoader, 5 elif lex.next("client"): @@ -207,6 +209,7 @@ registerTokens toktokSettings: # magics at = tokenize(handleMagics, '@') + `import` snippetJs snippetYaml snippetJson @@ -215,7 +218,6 @@ registerTokens toktokSettings: client `end` `include` - `import` = "import" fn = "fn" `func` = "func"