From 0a085343c4bbcc65de7cc8cf8af9cc74241d4b70 Mon Sep 17 00:00:00 2001 From: George Lemon Date: Wed, 6 Dec 2023 19:40:13 +0200 Subject: [PATCH] wip rework Signed-off-by: George Lemon --- .gitignore | 3 +- README.md | 164 +--- examples/templates/layouts/base.timl | 7 - examples/templates/partials/button.timl | 1 - examples/templates/views/index.timl | 18 - src/tim.nim | 318 +++---- src/tim.nims | 56 +- src/tim/engine/ast.nim | 458 ++++++++++ src/tim/engine/compiler.nim | 741 +++++++++++++++++ src/tim/engine/logging.nim | 236 ++++++ src/{timpkg => tim}/engine/meta.nim | 132 +-- src/tim/engine/parser.nim | 776 +++++++++++++++++ src/tim/engine/tokens.nim | 162 ++++ src/timpkg/commands/buildCommand.nim | 11 - src/timpkg/commands/initCommand.nim | 11 - src/timpkg/commands/setupEngine.nim | 73 -- src/timpkg/commands/watchCommand.nim | 11 - src/timpkg/commands/xmlCommand.nim | 11 - src/timpkg/engine/ast.nim | 302 ------- src/timpkg/engine/compiler.nim | 389 --------- src/timpkg/engine/data.nim | 197 ----- src/timpkg/engine/init.nim | 218 ----- src/timpkg/engine/parser.nim | 921 --------------------- src/timpkg/engine/private/jitutils.nim | 400 --------- src/timpkg/engine/private/stdcalls.nim | 18 - src/timpkg/engine/private/transpiler.nim | 433 ---------- src/timpkg/engine/resolver.nim | 160 ---- src/timpkg/engine/tokens.nim | 384 --------- src/timpkg/engine/utils.nim | 5 - tests/{examples => app}/storage/.gitkeep | 0 tests/app/templates/layouts/base.timl | 7 + tests/app/templates/views/index.timl | 2 + tests/config.nims | 4 +- tests/examples/templates/layouts/base.timl | 6 - tests/examples/templates/views/index.timl | 3 - tests/examples/templates/views/static.timl | 1 - tests/test1.nim | 114 +-- tim.nimble | 46 +- 38 files changed, 2710 insertions(+), 4089 deletions(-) delete mode 100644 examples/templates/layouts/base.timl delete mode 100644 examples/templates/partials/button.timl delete mode 100644 examples/templates/views/index.timl create mode 100644 src/tim/engine/ast.nim create mode 100644 src/tim/engine/compiler.nim create mode 100644 src/tim/engine/logging.nim rename src/{timpkg => tim}/engine/meta.nim (67%) create mode 100644 src/tim/engine/parser.nim create mode 100644 src/tim/engine/tokens.nim delete mode 100644 src/timpkg/commands/buildCommand.nim delete mode 100644 src/timpkg/commands/initCommand.nim delete mode 100644 src/timpkg/commands/setupEngine.nim delete mode 100644 src/timpkg/commands/watchCommand.nim delete mode 100644 src/timpkg/commands/xmlCommand.nim delete mode 100644 src/timpkg/engine/ast.nim delete mode 100644 src/timpkg/engine/compiler.nim delete mode 100644 src/timpkg/engine/data.nim delete mode 100644 src/timpkg/engine/init.nim delete mode 100644 src/timpkg/engine/parser.nim delete mode 100644 src/timpkg/engine/private/jitutils.nim delete mode 100644 src/timpkg/engine/private/stdcalls.nim delete mode 100644 src/timpkg/engine/private/transpiler.nim delete mode 100644 src/timpkg/engine/resolver.nim delete mode 100644 src/timpkg/engine/tokens.nim delete mode 100644 src/timpkg/engine/utils.nim rename tests/{examples => app}/storage/.gitkeep (100%) create mode 100644 tests/app/templates/layouts/base.timl create mode 100644 tests/app/templates/views/index.timl delete mode 100644 tests/examples/templates/layouts/base.timl delete mode 100644 tests/examples/templates/views/index.timl delete mode 100644 tests/examples/templates/views/static.timl diff --git a/.gitignore b/.gitignore index 7186fe5..00b50ad 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ nimblecache/ htmldocs/ /pkginfo.json /npm_modules -/package-lock.json \ No newline at end of file +/package-lock.json +/tests/app/storage/* \ No newline at end of file diff --git a/README.md b/README.md index 3e84fc0..5e8198f 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,51 @@ -

- Tim Engine
- ⚡️ A high-performance template engine & markup language inspired by the Emmet syntax.
- FastCompiled • Written in Nim language 👑 -

- -

- nimble install tim -

- -

- API reference

- Github Actions Github Actions -

- -

- -

- -## 😍 Key Features -- [x] `layouts`, `views` and `partials` logic -- [x] `Global`, `Scope`, and `Internal` variables -- [x] `for` Loop Statements -- [x] `if`/`elif`/`else` Conditional Statements -- [x] Partials via `@include` -- [ ] Mixins -- [ ] SEO / Semantic Checker -- [x] Language Extension `.timl` 😎 -- [x] Snippets 🎊 - * JavaScript 🥰 - * JSON 😍 - * YAML 🤩 w/ Built-in parser via Nyml - * SASS 🫠 w/ Built-in parser via `libsass` -- [x] Written in Nim language 👑 -- [x] Open Source | `MIT` License - -## 😎 Library features -- [x] Everything in **Key features** -- [x] `Global` and `Scope` data using `JSON` (`std/json` or `pkg/packedjson`) -- [x] Static transpilation to `HTML` files -- [x] ♨️ JIT Compilation via MsgPacked AST - -## 🌎 Standalone CLI -The CLI is a standalone cross-language application that transpiles your **Tim templates** into source code for the current (supported) language. - -Of course, the generated source code will not look very nice, but who cares, -since you'll always have your `.timl` sources and finally, your application will **render at super speed!** - -How many times have you heard _"Moustache is slow"_, or _"Pug.js compiling extremely slow"_, or _"...out of memory"_, -or _"Jinja being extremely slow when..."_? - -Well, that's no longer the case! - -### CLI Features -- [x] Everything in Basics -- [x] `Global` and `Scope` data using language -- [x] Cross-language -- [ ] `.timl` ➡ `.nim` -- [ ] ↳ `.js` -- [ ] ↳ `.rb` -- [ ] ↳ `.py` -- [ ] ↳ `.php` - - -## Setup in Nim with JIT Compilation +A fast, compiled, multi-threading templating engine and markup language written in Nim +Can be used from Nim, Node/Bun (as addon) or as a standalone CLI application. + +## Key features +- Fast, compiled, multi-threading +- Transpile `timl` to your favorite language [See Supported languages](#supported-languages) +- As a Nimble library for `Nim` development +- Available for **Node** & **Bun** [Tim Engine for NodeJS and Bun](#tim-for-javascript) +- Easy to learn, intuitive syntax +- Built-in Browser Sync & Reload +- Written in Nim language +- Open Source | MIT License + +## Examples +Tim requires the following directories to be created `layouts`, `views`, `partials`. Also, +pre-compile to binary AST and static HTML + +Using Tim as a Nimble library: + +```tim +div.container > div.row > div.col-12 + h1.display-3: "Tim is awesome!" + p: "This is Tim Engine, a fast template-engine & markup language" + for $x in $items: + span: $x +``` ```nim -import tim, tim/engine/meta -export render, precompile +import tim -var Tim*: TimEngine -Tim.init( - source = "./templates", # or ../templates if placed outside `src` directory - output = "./storage/templates", - minified = false, - indent = 4 -) +# Create a singleton of `Tim` +var timl = newTim("./templates", "./storage", currentSourcePath(), minify = true, indent = 2) -# Precompile your `.timl` templates at boot-time -Tim.precompile() - -# Render a specific view by name (filename, or subdir.filename_without_ext) -res.send(Tim.render("homepage")) +# tell Tim to precompile available `.timl` templates. +# this must be called once in the main state of your application +timl.precompile(flush = true, waitThread = true) +timl.render("index") ``` -## Snippets - -### JavaScript Snippets -Write JavaScript snippets or a component-based functionality direclty in your `.timl` file, using backticks. - -````tim -main > div.container > div.row > div.col-lg-4.mx-auto - @include "button" - - ```js -document.querySelector('button').addEventListener('click', function() { - console.log("yay!") -}); - ``` -```` - -### Sass Snippets -Built-in CSS support with SASS via `libsass` (install [libsass](https://github.com/sass/libsass) library) - -````tim -div.container.product > div.row > div.col-4.mx-auto - a.btn.cta-checkout > span: "Go to checkout" - - ```sass -div.product - btn - font-weight: bold - ``` -```` - -### Errors - - -``` -Error (57:4): The ID "schemaFieldEditor" is also used for another element at line 40 -/vasco/templates/views/system/list.timl -``` +### CLI +Work in progress -### ❤ Contributions & Support -- 🐛 Found a bug? [Create a new Issue](https://github.com/openpeeps/tim/issues) -- 👋 Wanna help? [Fork it!](https://github.com/openpeeps/tim/fork) +- 🐛 Found a bug? [Create a new Issue](https://github.com/openpeeps/bro/issues) +- 👋 Wanna help? [Fork it!](https://github.com/openpeeps/bro/fork) - 😎 [Get €20 in cloud credits from Hetzner](https://hetzner.cloud/?ref=Hm0mYGM9NxZ4) -- 🥰 [Donate via PayPal address](https://www.paypal.com/donate/?hosted_button_id=RJK3ZTDWPL55C) +- 🥰 [Donate to OpenPeeps via PayPal address](https://www.paypal.com/donate/?hosted_button_id=RJK3ZTDWPL55C) -### 🎩 License -Tim Engine | `MIT` license. [Made by Humans from OpenPeeps](https://github.com/openpeeps).
-Copyright © 2023 OpenPeeps & Contributors — All rights reserved. +## 🎩 License +Tim Engine is an Open Source software released under LGPLv3. Proudly made in 🇪🇺 Europe [by Humans from OpenPeeps](https://github.com/openpeeps). +Copyright © 2023 OpenPeeps & Contributors — All rights reserved. \ No newline at end of file diff --git a/examples/templates/layouts/base.timl b/examples/templates/layouts/base.timl deleted file mode 100644 index ec7f9fb..0000000 --- a/examples/templates/layouts/base.timl +++ /dev/null @@ -1,7 +0,0 @@ -html - head - meta charset="UTF-8" - title: "Tim Engine" - link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" - body - @view \ No newline at end of file diff --git a/examples/templates/partials/button.timl b/examples/templates/partials/button.timl deleted file mode 100644 index 3c4c5e5..0000000 --- a/examples/templates/partials/button.timl +++ /dev/null @@ -1 +0,0 @@ -div.mt-3 > button.btn.btn-light: "I love this!" \ No newline at end of file diff --git a/examples/templates/views/index.timl b/examples/templates/views/index.timl deleted file mode 100644 index f470167..0000000 --- a/examples/templates/views/index.timl +++ /dev/null @@ -1,18 +0,0 @@ -div.container > div.row.vh-100 > div.align-self-center - h3.h2: "Tim Engine is Awesome!" - p.h5.fw-light.text-muted: "A high-performance, compiled template engine & markup language" - @include "button" - // Vanilla JavaScript? Yes, please! - // Tim transpiles all JavaScript snippets and - // insert the final output before closing - ```js -document.querySelector('button').addEventListener('click', function() { - console.log("yay!") -}); - ``` - // SASS flavours? Oh, yeah! - ```sass -div.container - button - font-weight: bold - ``` \ No newline at end of file diff --git a/src/tim.nim b/src/tim.nim index 21143ff..99a7c4b 100644 --- a/src/tim.nim +++ b/src/tim.nim @@ -1,148 +1,186 @@ -# A high-performance compiled template engine -# inspired by the Emmet syntax. +# A super fast template engine for cool kids # -# (c) 2023 George Lemon | MIT License +# (c) 2023 George Lemon | LGPL License # Made by Humans from OpenPeeps # https://github.com/openpeeps/tim +import std/json except `%*` +import tim/engine/[meta, parser, compiler, logging] +import pkg/[watchout, kapsis/cli] + +from std/strutils import `%`, indent +from std/os import `/` + + +const + DOCKTYPE = "" + defaultLayout = "base" + +proc jitCompiler*(engine: Tim, tpl: TimTemplate, data: JsonNode): HtmlCompiler = + ## Compiles `tpl` AST at runtime + newCompiler(engine.readAst(tpl), tpl, engine.isMinified(), engine.getIndentSize(), data) + +proc displayErrors(l: Logger) = + for err in l.errors: + display(err) + display(l.filePath) + +proc compileCode*(engine: Tim, tpl: TimTemplate) = + # Compiles `tpl` TimTemplate to either `.html` or binary `.ast` + var p: Parser = engine.newParser(tpl) + if likely(not p.hasError): + if tpl.jitEnabled(): + # when enabled, will save the generated binary ast + # to disk for runtime computation. + engine.writeAst(tpl, p.getAst) + else: + # otherwise, compiles the generated AST and save + # a pre-compiled HTML version on disk + var c = newCompiler(p.getAst, tpl, engine.isMinified, engine.getIndentSize) + if likely(not c.hasError): + case tpl.getType: + of ttView: + engine.writeHtml(tpl, c.getHtml) + of ttLayout: + engine.writeHtml(tpl, c.getHead) + engine.writeHtmlTail(tpl, c.getTail) + else: discard + else: c.logger.displayErrors() + else: + p.logger.displayErrors() + +proc precompile*(engine: Tim, callback: TimCallback = nil, + flush = true, waitThread = false) = + ## Precompiles available templates inside `layouts` and `views` + ## directories to either static `.html` or binary `.ast`. + ## + ## Partials are not part of the precompilation processs. These + ## are include-only files that can be imported into layouts + ## or views via `@import` statement. + ## + ## By enabling flushing ensures outdated files are deleted. + if flush: engine.flush() + when not defined release: + when defined timHotCode: + var watchable: seq[string] + proc onFound(file: watchout.File) = + # echo indent(file.getName(), 3) + let tpl: TimTemplate = engine.getTemplateByPath(file.getPath()) + case tpl.getType + of ttView, ttLayout: + engine.compileCode(tpl) + if engine.errors.len > 0: + for err in engine.errors: + echo err + # setLen(engine.errors, 0) + else: discard + + proc onChange(file: watchout.File) = + echo "✨ Changes detected" + echo indent(file.getName() & "\n", 3) + let tpl: TimTemplate = engine.getTemplateByPath(file.getPath()) + case tpl.getType() + of ttView, ttLayout: + engine.compileCode(tpl) + if engine.errors.len > 0: + for err in engine.errors: + echo err + # setLen(engine.errors, 0) + else: + discard + # echo "getting dependencies" + + proc onDelete(file: watchout.File) = + discard + # echo "✨ Deleted\n", file.getName() + # echo "✨ Tim Engine - Syncing Templates" + var w = newWatchout(@[engine.getSourcePath() / "*"], onChange, onFound) + w.start(waitThread) + else: + for tpl in engine.getViews(): + engine.compileCode(tpl) + for tpl in engine.getLayouts(): + engine.compileCode(tpl) + else: + for tpl in engine.getViews(): + engine.compileCode(tpl) + for tpl in engine.getLayouts(): + engine.compileCode(tpl) + +proc render*(engine: Tim, viewName: string, + layoutName = defaultLayout, global, local = newJObject()): string = + ## Renders a view based on `viewName` and `layoutName`. + ## Exposing data to a template is possible using `global` or + ## `local` objects. + if engine.hasView(viewName): + var view: TimTemplate = engine.getView(viewName) + var data: JsonNode = newJObject() + if likely(engine.hasLayout(layoutName)): + var layout: TimTemplate = engine.getLayout(layoutName) + if not view.jitEnabled: + # render a pre-compiled HTML + result = DOCKTYPE + add result, layout.getHtml() + add result, indent(view.getHtml(), layout.getViewIndent) + add result, layout.getTail() + else: + # compile and render template at runtime + var data = newJObject() + data["global"] = global + data["local"] = local + result = DOCKTYPE + var layoutTail: string + if not layout.jitEnabled: + # when requested layout is pre-rendered + # will use the static HTML version from disk + add result, layout.getHtml() + layoutTail = layout.getTail() + else: + var clayout = engine.jitCompiler(layout, data) + if likely(not clayout.hasError): + add result, clayout.getHtml() + layoutTail = clayout.getTail() + else: + clayout.logger.displayErrors() + var cview = engine.jitCompiler(view, data) + if likely(not cview.hasError): + add result, indent(cview.getHtml(), layout.getViewIndent) + else: + cview.logger.displayErrors() + add result, layoutTail + else: + raise newException(TimError, "No layouts available") + else: + raise newException(TimError, "View not found: `$1`" % [viewName]) when defined napibuild: + # Setup for building Tim as a node addon via NAPI import pkg/denim - import std/[os, tables] - import std/json except `%*` - import ./timpkg/engine/[meta, parser, compiler] - - type - ErrorMessage = enum - errInitialized = "TimEngine is already initialied" - errInitFnArgs = "`init` function requires 4 arguments\n" - errNotInitialized = "TimEngine is not initialized" - errRenderFnArgs = "`render` function expect at least 1 argument\n" - errDefaultLayoutNotFound = "`base.timl` layout is missing from your `/layouts` directory" - - var timEngine: TimEngine - - const - errIdent = "TimEngine" - docktype = "" - defaultLayoutName = "base" - - template precompileCode() = - var p = timEngine.parse(t.getSourceCode, t.getFilePath, templateType = t.getType) - if p.hasError: - assert error(p.getError, errIdent) - return - if p.hasJit: - t.enableJIT - timEngine.writeAst(t, p.getStatements, timEngine.getIndent) - else: - var c = newCompiler(timEngine, p.getStatements, t, timEngine.shouldMinify, timEngine.getIndent, t.getFilePath) - if not c.hasError: - timEngine.writeHtml(t, c.getHtml) - - proc newJITCompilation(tp: Template, data: JsonNode, viewCode = "", hasViewCode = false): Compiler = - result = newCompiler(timEngine, timEngine.readAst(tp), - tpl = tp, - minify = timEngine.shouldMinify, - indent = timEngine.getIndent, - filePath = tp.getFilePath, - data = data, - viewCode = viewCode, - hasViewCode = hasViewCode - ) + from std/sequtils import toSeq + var timjs: Tim init proc(module: Module) = - proc init(source: string, output: string, indent: int, minify: bool, globals: object) {.export_napi.} = - ## Create an instance of TimEngine. - ## To be called in the main state of your application - if timEngine == nil: - timEngine.init( - source = args[0].getStr, - output = args[1].getStr, - indent = args[2].getInt, - minified = args[3].getBool - ) - timEngine.setData(args[4].tryGetJson) - else: assert error($errInitialized, errIdent) - - proc precompile() {.export_napi.} = - ## Export `precompile` function. - ## To be used in the main state of your application - if timEngine != nil: - for k, t in timEngine.getViews.mpairs: - precompileCode() - for k, t in timEngine.getLayouts.mpairs: - precompileCode() - else: assert error($errNotInitialized, errIdent) - - proc render(view: string, scope: object, layout: string): string {.export_napi.} = - ## Export `render` function - if timEngine != nil: - let - viewName = args[0].getStr - layoutName = - if args.len == 3: - if timEngine.hasLayout(args[2].getStr): - args[2].getStr - else: defaultLayoutName - else: defaultLayoutName - if layoutName == defaultLayoutName: - if not timEngine.hasLayout(defaultLayoutName): - assert error($errDefaultLayoutNotFound, errIdent) - - # echo args[1].expect(napi_object) - if timEngine.hasView(viewName): - var tpv, tpl: Template - # create a JsonNode to expose available data - var jsonData = newJObject() - jsonData["scope"] = - if args.len >= 2: - args[1].tryGetJson - else: newJObject() - tpv = timEngine.getView(viewName) - tpl = timEngine.getLayout(layoutName) - if tpv.isJitEnabled: - # when enabled, compiles timl code to HTML on the fly - var cview = newJITCompilation(tpv, jsonData) - var clayout = newJITCompilation(tpl, jsonData, cview.getHtml, hasViewCode = true) - # todo handle compiler warnings - return %*(docktype & clayout.getHtml) - else: assert error($errNotInitialized, errIdent) - -elif defined emscripten: - import std/json - import timpkg/engine/[meta, parser, compiler, ast] - - # https://emscripten.org/docs/api_reference/emscripten.h.html - proc emscripten_run_script(code: cstring) {.importc.} - - proc tim(code: cstring, minify: bool, indent = 2): cstring {.exportc.} = - var p = parser.parse($code) - if not p.hasError: - return cstring(newCompiler(p.getStatements, true, indent, data = %*{}).getHtml) - let jsError = "throw new Error('" & p.getError & "');" - emscripten_run_script(cstring(jsError)) - -elif isMainModule: - ## The standalone cross-language application - ## ==================== - ## This is Tim as command line interface. It can be used for transpiling - ## Tim sources to various programming/markup languages such as: - ## Nim, JavaScript, Python, XML, PHP, Go, Ruby, Java, Lua. - ## **Note**: This is work in progress - import kapsis - import ./timpkg/commands/[initCommand, watchCommand, buildCommand] - - App: - about: - "A High-performance, compiled template engine & markup language" - "Made by Humans from OpenPeep" - - commands: - $ "init": - ? "Generate a new Tim config" - $ "watch": - ? "Transpile and Watch for changes" - $ "build": - ? "Transpile Tim to targeting language" -else: - include timpkg/engine/init + proc init(src: string, output: string, + basepath: string, minify: bool, indent: int) {.export_napi.} = + ## Initialize Tim Engine + timjs = newTim( + args.get("src").getStr, + args.get("output").getStr, + args.get("basepath").getStr, + args.get("minify").getBool, + args.get("indent").getInt + ) + + proc precompileSync() {.export_napi.} = + ## Precompile Tim templates + timjs.precompile(flush = true, waitThread = false) + + proc renderSync(view: string) {.export_napi.} = + ## Render a `view` by name + let x = timjs.render(args.get("view").getStr) + return %*(x) + +elif not isMainModule: + import tim/engine/[meta, parser, compiler, logging] + + export parser, compiler, json + export meta except Tim \ No newline at end of file diff --git a/src/tim.nims b/src/tim.nims index df63523..5054523 100644 --- a/src/tim.nims +++ b/src/tim.nims @@ -1,48 +1,8 @@ -when defined emscripten: - # This path will only run if -d:emscripten is passed to nim. - --nimcache:tmp # Store intermediate files close by in the ./tmp dir. - --os:linux # Emscripten pretends to be linux. - --cpu:wasm32 # Emscripten is 32bits. - --cc:clang # Emscripten is very close to clang, so we ill replace it. - when defined(windows): - --clang.exe:emcc.bat # Replace C - --clang.linkerexe:emcc.bat # Replace C linker - --clang.cpp.exe:emcc.bat # Replace C++ - --clang.cpp.linkerexe:emcc.bat # Replace C++ linker. - else: - --clang.exe:emcc # Replace C - --clang.linkerexe:emcc # Replace C linker - --clang.cpp.exe:emcc # Replace C++ - --clang.cpp.linkerexe:emcc # Replace C++ linker. - when compileOption("threads"): - # We can have a pool size to populate and be available on page run - --passL:"-sPTHREAD_POOL_SIZE=2" - # discard - --listCmd # List what commands we are running so that we can debug them. - --gc:arc - --exceptions:goto - --define:noSignalHandler - # --objChecks:off # for some reason I get ObjectConversionDefect in std/streams - --checks:off - --define:danger - --define:release - --opt:speed - # --passC: "-flto" - # --passL: "-flto" - switch("passL", "-s ALLOW_MEMORY_GROWTH") - switch("passL", "-s INITIAL_MEMORY=512MB") - switch("passL", "-Os -o tim.html --shell-file src/tim.html") - switch("passL", "-s EXPORTED_FUNCTIONS=_free,_malloc,_tim") - switch("passL", "-s EXPORTED_RUNTIME_METHODS=ccall,cwrap,setValue,getValue,stringToUTF8,allocateUTF8,UTF8ToString") -else: - --threads:on - --define:useMalloc - --gc:arc - --deepcopy:on - --define:msgpack_obj_to_map - when defined release: - --define:danger - --opt:speed - --passC: "-flto" - --passL: "-flto" - --define:nimAllocPagesViaMalloc \ No newline at end of file +--mm:arc +--define:timHotCode +--threads:on + +when defined napibuild: + --define:napiOrWasm + --noMain:on + --passC:"-I/usr/include/node -I/usr/local/include/node" \ No newline at end of file diff --git a/src/tim/engine/ast.nim b/src/tim/engine/ast.nim new file mode 100644 index 0000000..63cb515 --- /dev/null +++ b/src/tim/engine/ast.nim @@ -0,0 +1,458 @@ +# 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 ./tokens +import std/[tables, json, macros] + +from std/htmlparser import tagToStr, htmlTag, HtmlTag +export tagToStr, htmlTag, HtmlTag + +when not defined release: + import std/jsonutils +else: + import pkg/jsony + +type + NodeType* = enum + ntInvalid + + ntLitInt = "int" + ntLitString = "string" + ntLitFloat = "float" + ntLitBool = "bool" + ntLitArray = "array" + ntLitObject = "object" + ntArrayStorage = "Array" + ntObjectStorage = "Object" + + ntVariableDef = "Variable" + ntFunctionDef = "Function" + ntAssignExpr = "Assignment" + ntHtmlElement = "HtmlElement" + ntInfixExpr = "InfixExpression" + ntMathInfixExpr = "MathExpression" + ntCommandStmt = "CommandStatement" + ntIdent = "Identifier" + ntDotExpr + ntBracketExpr + ntConditionStmt = "ConditionStatement" + ntLoopStmt = "LoopStmt" + ntViewLoader = "ViewLoader" + ntInclude = "Include" + + ntJavaScriptSnippet = "JavaScriptSnippet" + + CommandType* = enum + cmdEcho = "echo" + cmdReturn = "return" + + StorageType* = enum + scopeStorage + ## Data created inside a `timl` template. + ## Scope data can be accessed by identifier name + ## ``` + ## var say = "Hello" + ## echo $say + ## ``` + globalStorage + ## Data exposed globally using a `JsonNode` object + ## when initializing Tim Engine. Global data + ## can be accessed from any layout, view or partial + ## using the `$app` prefix + localStorage + ## Data exposed from a Controller using a `JsonNode` object is stored + ## in a local storage. Can be accessed from the current view, layout and its partials + ## using the `$this` prefix. + + InfixOp* {.pure.} = enum + None + EQ = "==" + NE = "!=" + GT = ">" + GTE = ">=" + LT = "<" + LTE = "<=" + AND = "and" + OR = "or" + AMP = "&" # string concat purpose + + MathOp* {.pure.} = enum + invalidCalcOp + mPlus = "+" + mMinus = "-" + mMulti = "*" + mDiv = "/" + mMod = "%" + + HtmlAttributes* = TableRef[string, seq[Node]] + ConditionBranch* = tuple[expr: Node, body: seq[Node]] + + Node* {.acyclic.} = ref object + case nt*: NodeType + of ntHtmlElement: + tag*: HtmlTag + stag*: string + attrs*: HtmlAttributes + nodes*: seq[Node] + of ntVariableDef: + varName*: string + varValue*, varMod*: Node + varType*: NodeType + varUsed*, varImmutable*: bool + of ntAssignExpr: + asgnIdent*: string + asgnVal*: Node + of ntInfixExpr: + infixOp*: InfixOp + infixLeft*, infixRight*: Node + of ntMathInfixExpr: + infixMathOp*: MathOp + infixMathLeft*, infixMathRight*: Node + of ntConditionStmt: + condIfBranch*: ConditionBranch + condElifBranch*: seq[ConditionBranch] + condElseBranch*: seq[Node] + of ntLoopStmt: + loopItem*: Node + loopItems*: Node + loopBody*: seq[Node] + of ntLitString: + sVal*: string + of ntLitInt: + iVal*: int + of ntLitFloat: + fVal*: float + of ntLitBool: + bVal*: bool + of ntArrayStorage: + arrayItems*: seq[Node] + of ntObjectStorage: + objectItems*: OrderedTableRef[string, Node] + of ntCommandStmt: + cmdType*: CommandType + cmdValue*: Node + of ntIdent: + identName*: string + of ntDotExpr: + storageType*: StorageType + lhs*, rhs*: Node + of ntJavaScriptSnippet: + jsCode*: string + of ntInclude: + includes*: seq[string] + else: discard + meta*: Meta + + ValueKind* = enum + jsonValue, nimValue + + Value* = object + case kind*: ValueKind + of jsonValue: + jVal*: JsonNode + of nimValue: + nVal*: Node + + Meta* = array[3, int] + ScopeTable* = TableRef[string, Node] + # PartialsTable* = TableRef[string, Ast] + PartialTable* = TableRef[string, Ast] + Ast* = object + nodes*: seq[Node] + partials*: PartialTable + +const ntAssignableSet* = {ntLitString, ntLitInt, ntLitFloat, ntLitBool} + +proc getInfixOp*(kind: TokenKind, isInfixInfix: bool): InfixOp = + result = + case kind: + of tkEQ: EQ + of tkNE: NE + of tkLT: LT + of tkLTE: LTE + of tkGT: GT + of tkGTE: GTE + of tkAmp: AMP + else: + if isInfixInfix: + case kind + of tkAndAnd, tkAnd: AND + of tkOROR, tkOR: OR + of tkAmp: AMP + else: None + else: None + +proc getInfixMathOp*(kind: TokenKind, isInfixInfix: bool): MathOp = + result = + case kind: + of tkPlus: mPlus + of tkMinus: mMinus + of tkMultiply: mMulti + of tkDivide: mDiv + of tkMod: mMod + else: invalidCalcOp + +proc getTag*(x: Node): string = + # todo use pkg/htmlparser + result = + case x.tag + of tagA: "a" + of tagAbbr: "abbr" + of tagAcronym: "acronym" + of tagAddress: "address" + of tagApplet: "applet" + of tagArea: "area" + of tagArticle: "article" + of tagAside: "aside" + of tagAudio: "audio" + of tagB: "b" + of tagBase: "base" + of tagBasefont: "basefont" + of tagBdi: "bdi" + of tagBdo: "bdo" + of tagBig: "big" + of tagBlockquote: "blockquote" + of tagBody: "body" + of tagBr: "br" + of tagButton: "button" + of tagCanvas: "canvas" + of tagCaption: "caption" + of tagCenter: "center" + of tagCite: "cite" + of tagCode: "code" + of tagCol: "col" + of tagColgroup: "colgroup" + of tagCommand: "command" + of tagDatalist: "datalist" + of tagDd: "dd" + of tagDel: "del" + of tagDetails: "details" + of tagDfn: "dfn" + of tagDialog: "dialog" + of tagDiv: "div" + of tagDir: "dir" + of tagDl: "dl" + of tagDt: "dt" + of tagEm: "em" + of tagEmbed: "embed" + of tagFieldset: "fieldset" + of tagFigcaption: "figcaption" + of tagFigure: "figure" + of tagFont: "font" + of tagFooter: "footer" + of tagForm: "form" + of tagFrame: "frame" + of tagFrameset: "frameset" + of tagH1: "h1" + of tagH2: "h2" + of tagH3: "h3" + of tagH4: "h4" + of tagH5: "h5" + of tagH6: "h6" + of tagHead: "head" + of tagHeader: "header" + of tagHgroup: "hgroup" + of tagHtml: "html" + of tagHr: "hr" + of tagI: "i" + of tagIframe: "iframe" + of tagImg: "img" + of tagInput: "input" + of tagIns: "ins" + of tagIsindex: "isindex" + of tagKbd: "kbd" + of tagKeygen: "keygen" + of tagLabel: "label" + of tagLegend: "legend" + of tagLi: "li" + of tagLink: "link" + of tagMap: "map" + of tagMark: "mark" + of tagMenu: "menu" + of tagMeta: "meta" + of tagMeter: "meter" + of tagNav: "nav" + of tagNobr: "nobr" + of tagNoframes: "noframes" + of tagNoscript: "noscript" + of tagObject: "object" + of tagOl: "ol" + of tagOptgroup: "optgroup" + of tagOption: "option" + of tagOutput: "output" + of tagP: "p" + of tagParam: "param" + of tagPre: "pre" + of tagProgress: "progress" + of tagQ: "q" + of tagRp: "rp" + of tagRt: "rt" + of tagRuby: "ruby" + of tagS: "s" + of tagSamp: "samp" + of tagScript: "script" + of tagSection: "section" + of tagSelect: "select" + of tagSmall: "small" + of tagSource: "source" + of tagSpan: "span" + of tagStrike: "strike" + of tagStrong: "strong" + of tagStyle: "style" + of tagSub: "sub" + of tagSummary: "summary" + of tagSup: "sup" + of tagTable: "table" + of tagTbody: "tbody" + of tagTd: "td" + of tagTextarea: "textarea" + of tagTfoot: "tfoot" + of tagTh: "th" + of tagThead: "thead" + of tagTime: "time" + of tagTitle: "title" + of tagTr: "tr" + of tagTrack: "track" + of tagTt: "tt" + of tagU: "u" + of tagUl: "ul" + of tagVar: "var" + of tagVideo: "video" + of tagWbr: "wbr" + else: x.stag # tagUnknown + +# +# AST to JSON convertors +# +proc `$`*(node: Node): string = + {.gcsafe.}: + when not defined release: + pretty(toJson(node), 2) + else: + toJson(node) + +proc `$`*(nodes: seq[Node]): string = + {.gcsafe.}: + when not defined release: + pretty(toJson(nodes), 2) + else: + toJson(nodes) + +proc `$`*(x: Ast): string = + {.gcsafe.}: + when not defined release: + pretty(toJson(x), 2) + else: + toJson(x) + +# +# AST Generators +# +proc newNode*(nt: static NodeType, tk: TokenTuple): Node = + Node(nt: nt, meta: [tk.line, tk.pos, tk.col]) + +proc newNode*(nt: static NodeType): Node = + Node(nt: nt) + +proc newString*(tk: TokenTuple): Node = + result = newNode(ntLitString, tk) + result.sVal = tk.value + +proc newInteger*(v: int, tk: TokenTuple): Node = + result = newNode(ntLitInt, tk) + result.iVal = v + +proc newFloat*(v: float, tk: TokenTuple): Node = + result = newNode(ntLitFloat, tk) + result.fVal = v + +proc newBool*(v: bool, tk: TokenTuple): Node = + result = newNode(ntLitBool, tk) + result.bVal = v + +proc newVariable*(varName: string, varValue: Node, tk: TokenTuple): Node = + result = newNode(ntVariableDef, tk) + result.varName = varName + result.varValue = varvalue + +proc newAssignment*(tk: TokenTuple, varValue: Node): Node = + result = newNode(ntAssignExpr, tk) + result.asgnIdent = tk.value + result.asgnVal = varValue + +proc newInfix*(lhs, rhs: Node, infixOp: InfixOp, tk: TokenTuple): Node = + result = newNode(ntInfixExpr, tk) + result.infixOp = infixOp + result.infixLeft = lhs + result.infixRight = rhs + +proc newCommand*(cmdType: CommandType, node: Node, tk: TokenTuple): Node = + ## Create a new command for `cmdType` + result = newNode(ntCommandStmt, tk) + result.cmdType = cmdType + result.cmdValue = node + +proc newIdent*(tk: TokenTuple): Node= + result = newNode(ntIdent, tk) + result.identName = tk.value + +proc newHtmlElement*(tag: HtmlTag, tk: TokenTuple): Node = + result = newNode(ntHtmlElement, tk) + result.tag = tag + case tag + of tagUnknown: + result.stag = tk.value + else: discard + +proc newCondition*(condIfBranch: ConditionBranch, tk: TokenTuple): Node = + result = newNode(ntConditionStmt, tk) + result.condIfBranch = condIfBranch + +proc newArray*(items: seq[Node] = @[]): Node = + ## Creates a new `Array` node + result = newNode(ntArrayStorage) + result.arrayItems = items + +proc toTimNode*(x: JsonNode): Node = + case x.kind + of JString: + result = newNode(ntLitString) + result.sVal = x.str + of JInt: + result = newNode(ntLitInt) + result.iVal = x.num + of JFloat: + result = newNode(ntLitFloat) + result.fVal = x.fnum + of JBool: + result = newNode(ntLitBool) + result.bVal = x.bval + of JObject: + result = newNode(ntObjectStorage) + result.objectItems = newOrderedTable[string, Node]() + for k, v in x: + result.objectItems[k] = toTimNode(v) + of JArray: + result = newNode(ntArrayStorage) + for v in x: + result.arrayItems.add(toTimNode(v)) + else: discard +# proc toTimNode(): NimNode = +# # https://github.com/nim-lang/Nim/blob/version-2-0/lib/pure/json.nim#L410 +# case x.kind +# of nnkBracket: +# # ntArrayStorage +# if x.len == 0: +# return newCall(bindSym"newArray") +# result = newNimNode(nnkBracket) +# of nnkTableConstr: +# discard +# else: discard # error? + +# macro `%*`*(x: untyped): untyped = +# ## Convert an expression to a Tim Node directly. +# ## This macro is similar with `%*` from std/json, +# ## except is generating Tim Nodes instead of JsonNode objects. +# result = toTimNode(x) \ No newline at end of file diff --git a/src/tim/engine/compiler.nim b/src/tim/engine/compiler.nim new file mode 100644 index 0000000..3345987 --- /dev/null +++ b/src/tim/engine/compiler.nim @@ -0,0 +1,741 @@ +# 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, options, terminal] + +import ./ast, ./logging + +from ./meta import Tim, TimTemplate, TimTemplateType, + getType, getSourcePath + +type + HtmlCompiler* = object + ast: Ast + tpl: TimTemplate + nl: string = "\n" + output: string + jsOutput: string + jsCodeExists: bool + start: bool + case tplType: TimTemplateType + of ttLayout: + head: string + else: discard + logger*: Logger + 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 + # self closing tags (such as `submit`, `img` and so on) + when not defined timStandalone: + globalScope: ScopeTable = ScopeTable() + data: JsonNode + +# Forward Declaration +proc evaluateNodes(c: var HtmlCompiler, nodes: seq[Node], scopetables: var seq[ScopeTable]) +proc typeCheck(c: var HtmlCompiler, x, node: Node): bool +proc mathInfixEvaluator(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]): Node + +proc hasError*(c: HtmlCompiler): bool = c.hasErrors + +proc baseIndent(c: HtmlCompiler, isize: int): int = + if c.indent == 2: + int(isize / c.indent) + else: + isize + +proc getIndent(c: HtmlCompiler, meta: Meta, skipbr = false): string = + case meta[1] + of 0: + if not c.stickytail: + if not skipbr: + add result, c.nl + else: + if not c.stickytail: + add result, c.nl + add result, indent("", c.baseIndent(meta[1])) + +when not defined timStandalone: + # + # Scope API + # + proc globalScope(c: var HtmlCompiler, key: string, node: Node) = + # Add `node` to global scope + c.globalScope[key] = node + + proc `+=`(scope: ScopeTable, key: string, node: Node) = + # Add `node` to current `scope` + scope[key] = node + + proc stack(c: var HtmlCompiler, key: string, node: Node, + scopetables: var seq[ScopeTable]) = + # Add `node` to either local or global scope + if scopetables.len > 0: + scopetables[^1][node.varName] = node + return + c.globalScope[key] = node + + proc getCurrentScope(c: var HtmlCompiler, + scopetables: var seq[ScopeTable]): ScopeTable = + # Returns the current `ScopeTable`. When not found, + # returns the `globalScope` ScopeTable + if scopetables.len > 0: + return scopetables[^1] # the last scope + return c.globalScope + + proc getScope(c: var HtmlCompiler, key: string, + scopetables: var seq[ScopeTable] + ): tuple[scopeTable: ScopeTable, index: int] = + # Walks (bottom-top) through available `scopetables`, and finds + # the closest `ScopeTable` that contains a node for given `key`. + # If found returns the ScopeTable followed by index (position). + if scopetables.len > 0: + for i in countdown(scopetables.high, scopetables.low): + if scopetables[i].hasKey(key): + return (scopetables[i], i) + if likely(c.globalScope.hasKey(key)): + result = (c.globalScope, 0) + + proc inScope(c: HtmlCompiler, key: string, scopetables: var seq[ScopeTable]): bool = + # Performs a quick search in the current `ScopeTable` + if scopetables.len > 0: + result = scopetables[^1].hasKey(key) + if not result: + return c.globalScope.hasKey(key) + + proc fromScope(c: var HtmlCompiler, key: string, + scopetables: var seq[ScopeTable]): Node = + # Retrieves a node by `key` from `scopetables` + let some = c.getScope(key, scopetables) + if some.scopeTable != nil: + return some.scopeTable[key] + + proc newScope(scopetables: var seq[ScopeTable]) = + ## Create a new Scope + scopetables.add(ScopeTable()) + + proc clearScope(scopetables: var seq[ScopeTable]) = + ## Clears the current (latest) ScopeTable + scopetables.delete(scopetables.high) + +# +# Forward Declaration +# +proc varExpr(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) + +# +# AST Evaluators +# +proc toString(node: Node): string = + result = + case node.nt + of ntLitString: node.sVal + of ntLitInt: $node.iVal + of ntLitFloat: $node.fVal + of ntLitBool: $node.bVal + else: "" + +proc toString(node: JsonNode): string = + result = + case node.kind + of JString: node.str + of JInt: $node.num + of JFloat: $node.fnum + of JBool: $node.bval + of JObject, JArray: $(node) + else: "null" + +proc toString(value: Value): string = + result = + case value.kind + of jsonValue: + value.jVal.toString() + of nimValue: + value.nVal.toString() + +proc print(val: Node) = + echo val + let meta = " ($1:$2) " % [$val.meta[0], $val.meta[2]] + stdout.styledWriteLine( + fgGreen, "Debug", + fgDefault, meta, + fgMagenta, $(val.nt), + fgDefault, "\n" & toString(val) + ) + +proc print(val: JsonNode, line, col: int) = + let meta = " ($1:$2) " % [$line, $col] + var kind: JsonNodeKind + var val = val + if val != nil: + kind = val.kind + else: + val = newJNull() + stdout.styledWriteLine( + fgGreen, "Debug", + fgDefault, meta, + fgMagenta, $(val.kind), + fgDefault, "\n" & toString(val) + ) + +proc evalJson(c: var HtmlCompiler, storage: JsonNode, lhs, rhs: Node): JsonNode = + # Evaluate a JSON node + if lhs == nil: + if likely(storage.hasKey(rhs.identName)): + return storage[rhs.identName] + else: + c.logger.error(undeclaredField, rhs.meta[0], rhs.meta[1], [rhs.identName]) + c.hasErrors = true + +proc evalStorage(c: var HtmlCompiler, node: Node): JsonNode = + case node.lhs.nt + of ntIdent: + if node.lhs.identName == "this": + return c.evalJson(c.data["local"], nil, node.rhs) + if node.lhs.identName == "app": + return c.evalJson(c.data["global"], nil, node.rhs) + else: discard + +proc walkAccessorStorage(c: var HtmlCompiler, + lhs, rhs: Node, scopetables: var seq[ScopeTable]): Node = + case lhs.nt + of ntObjectStorage: + try: + result = lhs.objectItems[rhs.identName] + except KeyError: + c.logger.error(undeclaredField, rhs.meta[0], rhs.meta[1], [rhs.identName]) + of ntDotExpr: + let x = c.walkAccessorStorage(lhs.lhs, lhs.rhs, scopetables) + if likely(x != nil): + return c.walkAccessorStorage(x, rhs, scopetables) + of ntIdent: + let x = c.fromScope(lhs.identName, scopetables) + if likely(x != nil): + result = c.walkAccessorStorage(x.varValue, rhs, scopetables) + of ntArrayStorage: + discard # todo handle accessor storage for arrays + else: discard + +proc dotEvaluator(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]): Node = + case node.storageType + of localStorage, globalStorage: + let x = c.evalStorage(node) + if likely(x != nil): + result = toTimNode(x) + of scopeStorage: + return c.walkAccessorStorage(node.lhs, node.rhs, scopetables) + +proc writeDotExpr(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = + let someValue: Node = c.dotEvaluator(node, scopetables) + if likely(someValue != nil): + add c.output, someValue.toString() + c.stickytail = true + +proc evalCmd(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = + var val: Node + case node.cmdValue.nt + of ntIdent: + let some = c.getScope(node.cmdValue.identName, scopetables) + if likely(some.scopeTable != nil): + val = some.scopeTable[node.cmdValue.identName].varValue + else: + compileErrorWithArgs(undeclaredVariable, [node.cmdValue.identName]) + of ntAssignableSet: + val = node.cmdValue + of ntMathInfixExpr: + val = c.mathInfixEvaluator(node.cmdValue, scopetables) + of ntDotExpr: + let someValue: Node = c.dotEvaluator(node.cmdValue, scopetables) + if likely(someValue != nil): + case node.cmdType + of cmdEcho: + print(someValue) + else: discard + return + else: discard + + case node.cmdType + of cmdEcho: + print(val) + else: discard + +proc infixEvaluator(c: var HtmlCompiler, lhs, rhs: Node, + infixOp: InfixOp, scopetables: var seq[ScopeTable]): bool = + # Evaluates infix expressions + case infixOp: + of EQ: + case lhs.nt: + of ntLitBool: + case rhs.nt + of ntLitBool: + result = lhs.bVal == rhs.bVal + else: discard + of ntLitString: + case rhs.nt + of ntLitString: + result = lhs.sVal == rhs.sVal + else: discard + of ntLitInt: + case rhs.nt + of ntLitInt: + result = lhs.iVal == rhs.iVal + of ntLitFloat: + result = toFloat(lhs.iVal) == rhs.fVal + else: discard + of ntLitFloat: + case rhs.nt + of ntLitFloat: + result = lhs.fVal == rhs.fVal + of ntLitInt: + result = lhs.fVal == toFloat(rhs.iVal) + else: discard + of ntIdent: + var lhs = c.fromScope(lhs.identName, scopetables) + case rhs.nt + of ntIdent: + var rhs = c.fromScope(rhs.identName, scopetables) + if lhs != nil and rhs != nil: + result = c.infixEvaluator(lhs.varValue, rhs.varValue, infixOp, scopetables) + else: + result = c.infixEvaluator(lhs.varValue, rhs, infixOp, scopetables) + of ntDotExpr: + let x = c.dotEvaluator(lhs, scopetables) + result = c.infixEvaluator(x, rhs, infixOp, scopetables) + else: discard + of GT: + case lhs.nt: + of ntLitInt: + case rhs.nt + of ntLitInt: + result = lhs.iVal > rhs.iVal + of ntLitFloat: + result = toFloat(lhs.iVal) > rhs.fVal + else: discard + of ntLitFloat: + case rhs.nt + of ntLitFloat: + result = lhs.fVal > rhs.fVal + of ntLitInt: + result = lhs.fVal > toFloat(rhs.iVal) + else: discard + else: discard # handle float + of GTE: + case lhs.nt: + of ntLitInt: + case rhs.nt + of ntLitInt: + result = lhs.iVal >= rhs.iVal + of ntLitFloat: + result = toFloat(lhs.iVal) >= rhs.fVal + else: discard + of ntLitFloat: + case rhs.nt + of ntLitFloat: + result = lhs.fVal >= rhs.fVal + of ntLitInt: + result = lhs.fVal >= toFloat(rhs.iVal) + else: discard + else: discard # handle float + of AND: + case lhs.nt + of ntInfixExpr: + var lh: bool = c.infixEvaluator(lhs.infixLeft, lhs.infixRight, lhs.infixOp, scopetables) + var rh: bool + if lh: + case rhs.nt + of ntInfixExpr: + rh = c.infixEvaluator(rhs.infixLeft, rhs.infixRight, rhs.infixOp, scopetables) + else: discard # todo + if rh: + return lh and rh + else: discard + of OR: + case lhs.nt + of ntInfixExpr: + var lh: bool = c.infixEvaluator(lhs.infixLeft, lhs.infixRight, lhs.infixOp, scopetables) + var rh: bool + case rhs.nt + of ntInfixExpr: + rh = c.infixEvaluator(rhs.infixLeft, rhs.infixRight, rhs.infixOp, scopetables) + else: discard # todo + return lh or rh + else: discard # todo + else: discard # todo + +proc getValue(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]): Node = + # Evaluates an identifier node + let some = c.getScope(node.identName, scopetables) + if likely(some.scopeTable != nil): + return some.scopeTable[node.identName].varValue + compileErrorWithArgs(undeclaredVariable, [node.identName]) + +# proc calc(c: var HtmlCompiler, lhs, rhs: Node, op: MathOp, var seq[ScopeTable]): Node = +# case infixOp: +# of mPlus: + +# else: discard + +proc mathInfixEvaluator(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]): Node = + ## Evaluates a math expression and returns + ## the total as a Node value + case node.infixMathOp + of mPlus: + case node.infixMathLeft.nt + of ntLitFloat: + result = newNode(ntLitFloat) + case node.infixMathRight.nt + of ntLitFloat: + result.fVal = node.infixMathLeft.fVal + node.infixMathRight.fVal + of ntLitInt: + result.fVal = node.infixMathLeft.fVal + toFloat(node.infixMathRight.iVal) + else: discard + of ntLitInt: + case node.infixMathRight.nt + of ntLitFloat: + result = newNode(ntLitFloat) + result.fVal = toFloat(node.infixMathLeft.iVal) + node.infixMathRight.fVal + of ntLitInt: + result = newNode(ntLitInt) + result.iVal = node.infixMathLeft.iVal + node.infixMathRight.iVal + else: discard + of ntIdent: + let x = c.getValue(node.infixMathLeft, scopetables) + # if likely(x != nil): + # case x.nt + # of ntLitInt: + # else: discard # error + else: discard + else: discard + +let + boolDefault = ast.newNode(ntLitBool) +boolDefault.bVal = true +let + strDefault = ast.newNode(ntLitString) + intDefault = ast.newNode(ntLitInt) + +template evalBranch(branch: Node, body: untyped) = + case branch.nt + of ntInfixExpr, ntMathInfixExpr: + if c.infixEvaluator(branch.infixLeft, branch.infixRight, + branch.infixOp, scopetables): + body + return # condition is thruty + of ntIdent: + if c.infixEvaluator(branch, boolDefault, EQ, scopetables): + body + return # condition is thruty + of ntDotExpr: + let x = c.dotEvaluator(branch, scopetables) + if likely(x != nil): + if c.infixEvaluator(x, boolDefault, EQ, scopetables): + body + return + else: discard + +proc evalCondition(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = + # Evaluates condition branches + evalBranch node.condIfBranch.expr: + 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) + if node.condElseBranch.len > 0: + # handle `else` branch + c.evaluateNodes(node.condElseBranch, scopetables) + +proc evalConcat(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = + case node.infixLeft.nt + of ntDotExpr: + c.writeDotExpr(node.infixLeft, scopetables) + of ntAssignableSet: + add c.output, node.infixLeft.toString() + of ntInfixExpr: + c.evalConcat(node.infixLeft, scopetables) + else: discard + + case node.infixRight.nt + of ntDotExpr: + c.writeDotExpr(node.infixRight, scopetables) + of ntAssignableSet: + add c.output, node.infixRight.toString() + of ntInfixExpr: + c.evalConcat(node.infixRight, scopetables) + of ntMathInfixExpr: + let someValue = c.mathInfixEvaluator(node.infixRight, scopetables) + if likely(someValue != nil): + add c.output, someValue.toString() + else: discard + +proc evalLoop(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = + # Evaluates a `for` loop + let some = c.getScope(node.loopItems.identName, scopetables) + if likely(some.scopeTable != nil): + let items = some.scopeTable[node.loopItems.identName] + case items.varValue.nt: + of ntLitString: + for x in items.varValue.sVal: + newScope(scopetables) + node.loopItem.varValue = ast.Node(nt: ntLitString, sVal: $(x)) + c.varExpr(node.loopItem, scopetables) + c.evaluateNodes(node.loopBody, scopetables) + clearScope(scopetables) + of ntArrayStorage: + for x in items.varValue.arrayItems: + newScope(scopetables) + node.loopItem.varValue = x + c.varExpr(node.loopItem, scopetables) + c.evaluateNodes(node.loopBody, scopetables) + clearScope(scopetables) + of ntObjectStorage: + for x, y in items.varValue.objectItems: + newScope(scopetables) + node.loopItem.varValue = y + c.varExpr(node.loopItem, scopetables) + c.evaluateNodes(node.loopBody, scopetables) + clearScope(scopetables) + else: + let x = @[ntLitString, ntLitArray, ntLitObject] + compileErrorWithArgs(typeMismatch, [$(items.varValue.nt), x.join(" ")]) + else: + compileErrorWithArgs(undeclaredVariable, [node.loopItems.identName]) + +proc typeCheck(c: var HtmlCompiler, x, node: Node): bool = + if unlikely(x.nt != node.nt): + compileErrorWithArgs(typeMismatch, [$(node.nt), $(x.nt)]) + result = true + +# +# Compile Handlers +# +proc varExpr(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = + # Evaluates a variable + if likely(not c.inScope(node.varName, scopetables)): + c.stack(node.varName, node, scopetables) + else: compileErrorWithArgs(varRedefine, [node.varName]) + +proc assignExpr(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = + let some = c.getScope(node.asgnIdent, scopetables) + if likely(some.scopeTable != nil): + let varNode = some.scopeTable[node.asgnIdent] + if likely(c.typeCheck(varNode.varValue, node.asgnVal)): + if likely(not varNode.varImmutable): + varNode.varValue = node.asgnVal + else: + compileErrorWithArgs(varImmutable, [varNode.varName]) + +# +# Html Handler +# +proc getId(c: HtmlCompiler, node: Node): string = + add result, indent("id=", 1) & "\"" + let attrNode = node.attrs["id"][0] + case attrNode.nt + of ntLitString: + add result, attrNode.sVal + else: discard # todo + add result, "\"" + +proc getAttrs(c: HtmlCompiler, attrs: HtmlAttributes): string = + var i = 0 + var skipQuote: bool + let len = attrs.len + for k, attrNodes in attrs: + var attrStr: seq[string] + add result, indent("$1=" % [k], 1) & "\"" + for attrNode in attrNodes: + case attrNode.nt + of ntAssignableSet: + add attrStr, attrNode.toString() + else: + discard + add result, attrStr.join(" ") + if not skipQuote and i != len: + add result, "\"" + else: + skipQuote = false + inc i + +const voidElements = [tagArea, tagBase, tagBr, tagCol, + tagEmbed, tagHr, tagImg, tagInput, tagLink, tagMeta, + tagParam, tagSource, tagTrack, tagWbr, tagCommand, + tagKeygen, tagFrame] + +template htmlblock(x: Node, body) = + block: + case c.minify: + of false: + if c.stickytail == true: + c.stickytail = false + add c.output, c.getIndent(node.meta) + if c.start: + c.start = false # todo find a better method to exclude inserting \n at start + else: discard + let t = x.getTag() + add c.output, "<" + add c.output, t + if x.attrs != nil: + if x.attrs.hasKey("id"): + add c.output, c.getId(x) + x.attrs.del("id") # not needed anymore + if x.attrs.len > 0: + add c.output, c.getAttrs(x.attrs) + add c.output, ">" + body + case x.tag + of voidElements: + discard + else: + case c.minify: + of false: + add c.output, c.getIndent(node.meta) + else: discard + add c.output, "" + c.stickytail = false + +proc htmlElement(c: var HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = + htmlblock node: + c.evaluateNodes(node.nodes, scopetables) + +proc evaluatePartials(c: var HtmlCompiler, includes: seq[string], scopetables: var seq[ScopeTable]) = + for x in includes: + if likely(c.ast.partials.hasKey(x)): + c.evaluateNodes(c.ast.partials[x].nodes, scopetables) + +proc evaluateNodes(c: var HtmlCompiler, nodes: seq[Node], scopetables: var seq[ScopeTable]) = + for i in 0..nodes.high: + case nodes[i].nt + of ntHtmlElement: + c.htmlElement(nodes[i], scopetables) + of ntIdent: + let x = c.getValue(nodes[i], scopetables) + if likely(x != nil): + add c.output, x.toString + c.stickytail = true + of ntDotExpr: + let someValue: Node = c.dotEvaluator(nodes[i], scopetables) + if likely(someValue != nil): + add c.output, someValue.toString() + c.stickytail = true + of ntVariableDef: + c.varExpr(nodes[i], scopetables) + of ntCommandStmt: + c.evalCmd(nodes[i], scopetables) + of ntAssignExpr: + c.assignExpr(nodes[i], scopetables) + of ntConditionStmt: + c.evalCondition(nodes[i], scopetables) + of ntLoopStmt: + c.evalLoop(nodes[i], scopetables) + of ntLitString, ntLitInt, ntLitFloat, ntLitBool: + add c.output, nodes[i].toString + c.stickytail = true + of ntInfixExpr: + case nodes[i].infixOp + of AMP: + c.evalConcat(nodes[i], scopetables) + else: discard # todo + of ntViewLoader: + # add c.output, c.getIndent(nodes[i].meta) + c.head = c.output + reset(c.output) + of ntInclude: + c.evaluatePartials(nodes[i].includes, scopetables) + of ntJavaScriptSnippet: + add c.jsOutput, nodes[i].jsCode + if not c.jsCodeExists: + c.jsCodeExists = true + else: discard + +# +# Public API +# + +when not defined timStandalone: + proc newCompiler*(ast: Ast, tpl: TimTemplate, minify = true, + indent = 2, data: JsonNode = newJObject()): HtmlCompiler = + ## Create a new instance of `HtmlCompiler` + assert indent in [2, 4] + result = + HtmlCompiler( + ast: ast, + tpl: tpl, + start: true, + tplType: tpl.getType, + logger: Logger(filePath: tpl.getSourcePath()), + data: data, + minify: minify + ) + if minify: setLen(result.nl, 0) + var scopetables = newSeq[ScopeTable]() + result.evaluateNodes(result.ast.nodes, scopetables) +else: + proc newCompiler*(ast: Ast, tpl: TimTemplate, + minify = true, indent = 2): HtmlCompiler = + ## Create a new instance of `HtmlCompiler` + assert indent in [2, 4] + result = + HtmlCompiler( + ast: ast, + tpl: tpl, + start: true, + tplType: tpl.getType, + logger: Logger(filePath: tpl.getSourcePath()), + minify: minify + ) + if minify: setLen(result.nl, 0) + var scopetables = newSeq[ScopeTable]() + result.evaluateNodes(result.ast.nodes, scopetables) + +proc newCompiler*(ast: Ast, minify = true, indent = 2): HtmlCompiler = + ## Create a new instance of `HtmlCompiler + assert indent in [2, 4] + var c = HtmlCompiler( + ast: ast, + start: true, + tplType: ttView, + logger: Logger(), + minify: minify + ) + if minify: setLen(result.nl, 0) + var scopetables = newSeq[ScopeTable]() + c.evaluateNodes(c.ast.nodes, scopetables) + return c + +proc getHtml*(c: HtmlCompiler): string = + ## Get the compiled HTML + result = c.output + if c.tplType == ttView: + case c.jsCodeExists + of true: + add result, "\n" & "" + else: discard + +proc getHead*(c: HtmlCompiler): string = + ## Returns the top of a split layout + assert c.tplType == ttLayout + result = c.head + +proc getTail*(c: HtmlCompiler): string = + ## Retruns the tail of a layout + assert c.tplType == ttLayout + case c.jsCodeExists + of true: + result = "\n" & "" + add result, c.getHtml + else: + result = c.getHtml \ No newline at end of file diff --git a/src/tim/engine/logging.nim b/src/tim/engine/logging.nim new file mode 100644 index 0000000..8ada80a --- /dev/null +++ b/src/tim/engine/logging.nim @@ -0,0 +1,236 @@ +# A super fast template engine for cool kids +# +# (c) 2023 George Lemon | LGPL License +# Made by Humans from OpenPeeps +# https://github.com/openpeeps/tim + +from ./tokens import TokenTuple +from ./ast import Meta + +import std/[sequtils, strutils] + +when compileOption("app", "console"): + import pkg/kapsis/cli + +type + Message* = enum + invalidIndentation = "Invalid indentation" + unexpectedToken = "Unexpected token $" + undeclaredVariable = "Undeclared variable $" + varRedefine = "Attempt to redefine variable $" + varImmutable = "Attempt to reassign value to immutable constant $" + badIndentation = "Nestable statement requires indentation" + invalidContext = "Invalid $ in this context" + invalidViewLoader = "Invalid use of `@view` in this context. Use a layout instead" + duplicateViewLoader = "Duplicate `@view` loader" + typeMismatch = "Type mismatch. Got $ expected $" + duplicateAttribute = "Duplicate HTML attribute $" + duplicateField = "Duplicate field $" + undeclaredField = "Undeclared field $" + internalError = "$" + + Level* = enum + lvlInfo + lvlNotice + lvlWarn + lvlError + + Log* = ref object + msg: Message + extraLabel: string + line, col: int + useFmt: bool + args, extraLines: seq[string] + + Logger* = ref object + filePath*: string + infoLogs*, noticeLogs*, warnLogs*, errorLogs*: seq[Log] + +proc add(logger: Logger, lvl: Level, msg: Message, line, col: int, + useFmt: bool, args: varargs[string]) = + let log = Log(msg: msg, args: args.toSeq(), + line: line, col: col, useFmt: useFmt) + case lvl + of lvlInfo: + logger.infoLogs.add(log) + of lvlNotice: + logger.noticeLogs.add(log) + of lvlWarn: + logger.warnLogs.add(log) + of lvlError: + logger.errorLogs.add(log) + +proc add(logger: Logger, lvl: Level, msg: Message, line, col: int, useFmt: bool, + extraLines: seq[string], extraLabel: string, args: varargs[string]) = + let log = Log( + msg: msg, + args: args.toSeq(), + line: line, + col: col + 1, + useFmt: useFmt, + extraLines: extraLines, + extraLabel: extraLabel + ) + case lvl: + of lvlInfo: + logger.infoLogs.add(log) + of lvlNotice: + logger.noticeLogs.add(log) + of lvlWarn: + logger.warnLogs.add(log) + of lvlError: + logger.errorLogs.add(log) + +proc getMessage*(log: Log): Message = + result = log.msg + +proc newInfo*(logger: Logger, msg: Message, line, col: int, + useFmt: bool, args:varargs[string]) = + logger.add(lvlInfo, msg, line, col, useFmt, args) + +proc newNotice*(logger: Logger, msg: Message, line, col: int, + useFmt: bool, args:varargs[string]) = + logger.add(lvlNotice, msg, line, col, useFmt, args) + +proc newWarn*(logger: Logger, msg: Message, line, col: int, + useFmt: bool, args:varargs[string]) = + logger.add(lvlWarn, msg, line, col, useFmt, args) + +proc newError*(logger: Logger, msg: Message, line, col: int, useFmt: bool, args:varargs[string]) = + logger.add(lvlError, msg, line, col, useFmt, args) + +proc newErrorMultiLines*(logger: Logger, msg: Message, line, col: int, + useFmt: bool, extraLines: seq[string], extraLabel: string, args:varargs[string]) = + logger.add(lvlError, msg, line, col, useFmt, extraLines, extraLabel, args) + +proc newWarningMultiLines*(logger: Logger, msg: Message, line, col: int, + useFmt: bool, extraLines: seq[string], extraLabel: string, args:varargs[string]) = + logger.add(lvlWarn, msg, line, col, useFmt, extraLines, extraLabel, args) + +template warn*(msg: Message, tk: TokenTuple, args: varargs[string]) = + p.logger.newWarn(msg, tk.line, tk.pos, false, args) + +template warn*(msg: Message, tk: TokenTuple, strFmt: bool, args: varargs[string]) = + p.logger.newWarn(msg, tk.line, tk.pos, true, args) + +proc warn*(logger: Logger, msg: Message, line, col: int, args: varargs[string]) = + logger.add(lvlWarn, msg, line, col, false, args) + +proc warn*(logger: Logger, msg: Message, line, col: int, strFmt: bool, args: varargs[string]) = + logger.add(lvlWarn, msg, line, col, true, args) + +template warnWithArgs*(msg: Message, tk: TokenTuple, args: openarray[string]) = + if not p.hasErrors: + p.logger.newWarn(msg, tk.line, tk.pos, true, args) + +template error*(msg: Message, tk: TokenTuple) = + if not p.hasErrors: + p.logger.newError(msg, tk.line, tk.pos, false) + p.hasErrors = true + return # block code execution + +template error*(msg: Message, tk: TokenTuple, args: openarray[string]) = + if not p.hasErrors: + p.logger.newError(msg, tk.line, tk.pos, false, args) + p.hasErrors = true + return # block code execution + +template error*(msg: Message, tk: TokenTuple, strFmt: bool, + extraLines: seq[string], extraLabel: string, args: varargs[string]) = + if not p.hasErrors: + newErrorMultiLines(p.logger, msg, tk.line, tk.pos, strFmt, extraLines, extraLabel, args) + p.hasErrors = true + return # block code execution + +template errorWithArgs*(msg: Message, tk: TokenTuple, args: openarray[string]) = + if not p.hasErrors: + p.logger.newError(msg, tk.line, tk.pos, true, args) + p.hasErrors = true + return # block code execution + +template compileErrorWithArgs*(msg: Message, args: openarray[string]) = + c.logger.newError(msg, node.meta[0], node.meta[1], true, args) + c.hasErrors = true + return + +template compileErrorWithArgs*(msg: Message) = + c.logger.newError(msg, node.meta[0], node.meta[1], true, []) + c.hasErrors = true + return + +proc error*(logger: Logger, msg: Message, line, col: int, args: varargs[string]) = + logger.add(lvlError, msg, line, col, false, args) + +when defined napiOrWasm: + proc runIterator(i: Log, label = ""): string = + if label.len != 0: + add result, label + add result, "(" & $i.line & ":" & $i.col & ")" & spaces(1) + if i.useFmt: + var x: int + var str = split($i.msg, "$") + let length = count($i.msg, "$") - 1 + for s in str: + add result, s.strip() + if length >= x: + add result, indent(i.args[x], 1) + inc x + else: + add result, $i.msg + for a in i.args: + add result, a + + proc `$`*(i: Log): string = + runIterator(i) + + iterator warnings*(logger: Logger): string = + for i in logger.warnLogs: + yield runIterator(i, "Warning") + + iterator errors*(logger: Logger): string = + for i in logger.errorLogs: + yield runIterator(i) + if i.extraLines.len != 0: + if i.extraLabel.len != 0: + var extraLabel = "\n" + add extraLabel, indent(i.extraLabel, 6) + yield extraLabel + for extraLine in i.extraLines: + var extra = "\n" + add extra, indent(extraLine, 12) + yield extra + +elif compileOption("app", "console"): + proc runIterator(i: Log, label: string, fgColor: ForegroundColor): Row = + add result, span(label, fgColor, indentSize = 0) + add result, span("(" & $i.line & ":" & $i.col & ")") + if i.useFmt: + var x: int + var str = split($i.msg, "$") + let length = count($i.msg, "$") - 1 + for s in str: + add result, span(s.strip()) + if length >= x: + add result, span(i.args[x], fgBlue) + inc x + else: + add result, span($i.msg) + for a in i.args: + add result, span(a, fgBlue) + + iterator warnings*(logger: Logger): Row = + for i in logger.warnLogs: + yield runIterator(i, "Warning", fgYellow) + + iterator errors*(logger: Logger): Row = + for i in logger.errorLogs: + yield runIterator(i, "Error", fgRed) + if i.extraLines.len != 0: + if i.extraLabel.len != 0: + var extraLabel: Row + extraLabel.add(span(i.extraLabel, indentSize = 6)) + yield extraLabel + for extraLine in i.extraLines: + var extra: Row + extra.add(span(extraLine, indentSize = 12)) + yield extra \ No newline at end of file diff --git a/src/timpkg/engine/meta.nim b/src/tim/engine/meta.nim similarity index 67% rename from src/timpkg/engine/meta.nim rename to src/tim/engine/meta.nim index 4aa0899..c29fe0c 100644 --- a/src/timpkg/engine/meta.nim +++ b/src/tim/engine/meta.nim @@ -1,41 +1,40 @@ -# A blazing fast, cross-platform, multi-language -# template engine and markup language written in Nim. +# A super fast template engine for cool kids # -# Made by Humans from OpenPeeps -# (c) George Lemon | LGPLv3 License -# https://github.com/openpeeps/tim +# (c) 2023 George Lemon | LGPL License +# Made by Humans from OpenPeeps +# https://github.com/openpeeps/tim import std/[macros, os, json, strutils, base64, tables] -import pkg/checksums/md5 +import pkg/[checksums/md5, supersnappy, flatty] export getProjectPath -from ./ast import Tree +from ./ast import Ast when defined timStandalone: type Globals* = ref object of RootObj type - TemplateType* = enum + TimTemplateType* = enum ttLayout = "layouts" ttView = "views" ttPartial = "partials" TemplateSourcePaths = tuple[src, ast, html: string] - Template* = ref object - ast*: Tree + TimTemplate* = ref object + # ast*: Ast templateId: string - templateJit: bool + jit: bool templateName: string - case templateType: TemplateType + case templateType: TimTemplateType of ttPartial: discard of ttLayout: - discard + viewIndent: uint else: discard sources*: TemplateSourcePaths - TemplateTable = TableRef[string, Template] + TemplateTable = TableRef[string, TimTemplate] TimCallback* = proc() {.nimcall, gcsafe.} Tim* = ref object @@ -57,20 +56,7 @@ type TimError* = object of CatchableError -# proc setPlaceholderIndent*(t: var Template, pos: int) = -# t.placeholderIndent = pos - -# proc setPlaceHolderId*(t: var Template, pos: int): string = -# t.setPlaceholderIndent pos -# result = "$viewHandle_" & t.id & "" - -# proc getPlaceholderId*(t: Template): string = -# result = "viewHandle_" & t.id & "" - -# proc getPlaceholderIndent*(t: var Template): int = -# result = t.placeholderIndent - -proc getPath(engine: Tim, key: string, templateType: TemplateType): string = +proc getPath(engine: Tim, key: string, templateType: TimTemplateType): string = ## Retrieve path key for either a partial, view or layout var k: string var tree: seq[string] @@ -108,68 +94,85 @@ proc getAstStoragePath*(engine: Tim): string = result = engine.output / "ast" # -# Template API +# TimTemplate API # -proc newTemplate(id: string, templateType: TemplateType, - sources: TemplateSourcePaths): Template = - Template(templateId: id, templateType: templateType, sources: sources) +proc newTemplate(id: string, templateType: TimTemplateType, + sources: TemplateSourcePaths): TimTemplate = + TimTemplate(templateId: id, templateType: templateType, sources: sources) -proc getType*(t: Template): TemplateType = +proc getType*(t: TimTemplate): TimTemplateType = t.templateType -proc getHash*(t: Template): string = +proc getHash*(t: TimTemplate): string = hashid(t.sources.src) -proc getName*(t: Template): string = +proc getName*(t: TimTemplate): string = t.templateName -proc getTemplateId*(t: Template): string = +proc getTemplateId*(t: TimTemplate): string = t.templateId -proc writeHtml*(engine: Tim, tpl: Template, htmlCode: string) = +proc setViewIndent*(t: TimTemplate, i: uint) = + assert t.templateType == ttLayout + t.viewIndent = i + +proc getViewIndent*(t: TimTemplate): uint = + assert t.templateType == ttLayout + t.viewIndent + +proc writeHtml*(engine: Tim, tpl: TimTemplate, htmlCode: string) = ## Writes `htmlCode` on disk using `tpl` info writeFile(tpl.sources.html, htmlCode) -proc writeHtmlTail*(engine: Tim, tpl: Template, htmlCode: string) = +proc writeHtmlTail*(engine: Tim, tpl: TimTemplate, htmlCode: string) = ## Writes `htmlCode` tails on disk using `tpl` info writeFile(tpl.sources.html.changeFileExt("tail"), htmlCode) -proc writeAst*(engine: Tim, tpl: Template, astCode: Tree) = +proc writeAst*(engine: Tim, tpl: TimTemplate, astCode: Ast) = ## Writes `astCode` on disk using `tpl` info - # writeFile(tpl.sources.ast, tpl.tree) - discard - -proc getSourcePath*(t: Template): string = - ## Returns the absolute source path of `t` Template + writeFile(tpl.sources.ast, supersnappy.compress(flatty.toFlatty(astCode))) + +proc readAst*(engine: Tim, tpl: TimTemplate): Ast = + ## Get `AST` of `tpl` TimTemplate from storage + try: + let binAst = readFile(tpl.sources.ast) + result = flatty.fromFlatty(supersnappy.uncompress(binAst), Ast) + except IOError: + discard + +proc getSourcePath*(t: TimTemplate): string = + ## Returns the absolute source path of `t` TimTemplate result = t.sources.src -proc getAstPath*(t: Template): string = - ## Returns the absolute `html` path of `t` Template +proc getAstPath*(t: TimTemplate): string = + ## Returns the absolute `html` path of `t` TimTemplate result = t.sources.ast -proc getHtmlPath*(t: Template): string = - ## Returns the absolute `ast` path of `t` Template +proc getHtmlPath*(t: TimTemplate): string = + ## Returns the absolute `ast` path of `t` TimTemplate result = t.sources.html -proc enableJIT*(t: Template) = - t.templateJit = true +proc jitEnable*(t: TimTemplate) = + if not t.jit: t.jit = true -proc hasjit*(t: Template): bool = - t.templateJit +proc jitEnabled*(t: TimTemplate): bool = t.jit -proc getHtml*(t: Template): string = - ## Returns precompiled static HTML of `t` Template - result = readFile(t.getHtmlPath) +proc getHtml*(t: TimTemplate): string = + ## Returns precompiled static HTML of `t` TimTemplate + try: + result = readFile(t.getHtmlPath) + except IOError: + result = "" -proc getTail*(t: Template): string = +proc getTail*(t: TimTemplate): string = ## Returns the tail of a split layout result = readFile(t.getHtmlPath.changeFileExt("tail")) -iterator getViews*(engine: Tim): Template = +iterator getViews*(engine: Tim): TimTemplate = for id, tpl in engine.views: yield tpl -iterator getLayouts*(engine: Tim): Template = +iterator getLayouts*(engine: Tim): TimTemplate = for id, tpl in engine.layouts: yield tpl @@ -177,7 +180,7 @@ iterator getLayouts*(engine: Tim): Template = # Tim Engine API # -proc getTemplateByPath*(engine: Tim, path: string): Template = +proc getTemplateByPath*(engine: Tim, path: string): TimTemplate = ## Search for `path` in `layouts` or `views` table let id = hashid(path) # todo extract parent dir from path? if engine.views.hasKey(path): @@ -201,16 +204,16 @@ proc hasLayout*(engine: Tim, key: string): bool = ## Determine if `key` exists in `layouts` table result = engine.layouts.hasKey(engine.getPath(key, ttLayout)) -proc getLayout*(engine: Tim, key: string): Template = - ## Returns a `Template` layout with `layoutName` +proc getLayout*(engine: Tim, key: string): TimTemplate = + ## Returns a `TimTemplate` layout with `layoutName` result = engine.layouts[engine.getPath(key, ttLayout)] proc hasView*(engine: Tim, key: string): bool = ## Determine if `key` exists in `views` table result = engine.views.hasKey(engine.getPath(key, ttView)) -proc getView*(engine: Tim, key: string): Template = - ## Returns a `Template` view with `key` +proc getView*(engine: Tim, key: string): TimTemplate = + ## Returns a `TimTemplate` view with `key` result = engine.views[engine.getPath(key, ttView)] proc newTim*(src, output, basepath: string, @@ -237,7 +240,8 @@ proc newTim*(src, output, basepath: string, ) for sourceDir in [ttLayout, ttView, ttPartial]: - discard existsOrCreateDir(result.src / $sourceDir) + if not dirExists(result.src / $sourceDir): + raise newException(TimError, "Missing $1 directory: \n$2" % [$sourceDir, result.src / $sourceDir]) for fpath in walkDirRec(result.src / $sourceDir): let id = hashid(fpath) diff --git a/src/tim/engine/parser.nim b/src/tim/engine/parser.nim new file mode 100644 index 0000000..257c7b6 --- /dev/null +++ b/src/tim/engine/parser.nim @@ -0,0 +1,776 @@ +# A super fast template engine for cool kids +# +# (c) 2023 George Lemon | LGPL License +# Made by Humans from OpenPeeps +# https://github.com/openpeeps/tim + +{.warning[ImplicitDefaultValue]:off.} +import std/[macros, streams, lexbase, strutils, re, tables] +from std/os import `/` + +import ./tokens, ./ast, ./logging +from meta import Tim, TimTemplate, TimTemplateType, getType, + getTemplateByPath, getSourcePath, setViewIndent, jitEnable + +import pkg/importer + +type + Parser* = object + lex: Lexer + prev, curr, next: TokenTuple + engine: Tim + tpl: TimTemplate + logger*: Logger + hasErrors*, nilNotError, hasLoadedView: bool + tree: Ast + parentNode: seq[Node] + lvl: int + includes: seq[string] # a seq of `ntInclude` nodes + isMain: bool + + PrefixFunction = proc(p: var Parser, excludes, includes: set[TokenKind] = {}): Node {.gcsafe.} + InfixFunction = proc(p: var Parser, lhs: Node): Node {.gcsafe.} + +const + tkCompSet = {tkEQ, tkNE, tkGT, tkGTE, tkLT, tkLTE, tkAmp, tkAndAnd} + tkMathSet = {tkPlus, tkMinus, tkMultiply, tkDivide} + tkAssignableSet = { + tkString, tkBool, tkFloat, tkInteger, + tkIdentVar, tkLC, tkLB + } + tkComparable = tkAssignableSet + +# +# Forward Declaration +# +proc getPrefixFn(p: var Parser, excludes, includes: set[TokenKind] = {}): PrefixFunction {.gcsafe.} +proc getInfixFn(p: var Parser, excludes, includes: set[TokenKind] = {}): InfixFunction {.gcsafe.} + +proc parseInfix(p: var Parser, lhs: Node): Node {.gcsafe.} +proc getPrefixOrInfix(p: var Parser, includes, excludes: set[TokenKind] = {}, infix: Node = nil): Node {.gcsafe.} +proc parsePrefix(p: var Parser, excludes, includes: set[TokenKind] = {}): Node {.gcsafe.} + +proc pAnoArray(p: var Parser, excludes, includes: set[TokenKind] = {}): Node {.gcsafe.} +proc pAnoObject(p: var Parser, excludes, includes: set[TokenKind] = {}): Node {.gcsafe.} +proc pAssignable(p: var Parser): Node {.gcsafe.} + +template caseNotNil*(x: Node, body): untyped = + if likely(x != nil): + body + else: return nil + +# +# Error API +# +proc hasError*(p: Parser): bool = p.hasErrors + +# +# Parse Utils +# +proc isChild(tk, parent: TokenTuple): bool {.inline.} = + tk.pos > parent.pos and (tk.line > parent.line and tk.kind != tkEOF) + +proc isInfix*(p: var Parser): bool {.inline.} = + p.curr.kind in tkCompSet + tkMathSet + +proc isInfix*(tk: TokenTuple): bool {.inline.} = + tk.kind in tkCompSet + tkMathSet + +proc `isnot`(tk: TokenTuple, kind: TokenKind): bool {.inline.} = + tk.kind != kind + +proc `is`(tk: TokenTuple, kind: TokenKind): bool {.inline.} = + tk.kind == kind + +proc `in`(tk: TokenTuple, kind: set[TokenKind]): bool {.inline.} = + tk.kind in kind + +proc `notin`(tk: TokenTuple, kind: set[TokenKind]): bool {.inline.} = + tk.kind notin kind + +template expectWalk(kind: TokenKind) = + if likely(p.curr is kind): + walk p + else: return nil + +template expect(kind: TokenKind, body) = + if likely(p.curr is kind): + body + else: return nil + +proc isIdent(tk: TokenTuple, anyIdent, anyStringKey = false): bool = + result = tk is tkIdentifier + if result or (anyIdent and tk.kind != tkString): + return tk.value.validIdentifier + if result or anyStringKey: + return tk.value.validIdentifier + +proc walk(p: var Parser, offset = 1) {.gcsafe.} = + var i = 0 + while offset > i: + inc i + p.prev = p.curr + p.curr = p.next + p.next = p.lex.getToken() + case p.next.kind + of tkComment: + p.next = p.lex.getToken() # skip inline comments + else: discard + +macro prefixHandle(name: untyped, body: untyped) = + # Create a new prefix procedure with `name` and `body` + name.newProc( + [ + ident("Node"), # return type + nnkIdentDefs.newTree( + ident("p"), + nnkVarTy.newTree(ident("Parser")), + newEmptyNode() + ), + nnkIdentDefs.newTree( + ident("excludes"), + ident("includes"), + nnkBracketExpr.newTree(ident("set"), ident("TokenKind")), + newNimNode(nnkCurly) + ), + ], + body, + pragmas = nnkPragma.newTree(ident("gcsafe")) + ) + +proc includePartial(p: var Parser, node: Node, s: string) = + node.includes.add("/" & s & ".timl") + node.meta = [p.curr.line, p.curr.pos, p.curr.col] + p.includes.add(s & ".timl") + +proc getStorageType(p: var Parser): StorageType = + if p.curr.value in ["this", "app"]: + p.tpl.jitEnable() + if p.curr.value == "this": + return localStorage + result = globalStorage + +# +# Parse Handlers +# +prefixHandle pString: + # parse a string + result = ast.newString(p.curr) + walk p + +prefixHandle pInt: + # parse an interger + let v = + try: + parseInt(p.curr.value) + except ValueError: + return nil + result = ast.newInteger(v, p.curr) + walk p + +prefixHandle pFloat: + # parse a float number + let v = + try: + parseFloat(p.curr.value) + except ValueError: + return nil + result = ast.newFloat(v, p.curr) + walk p + +prefixHandle pBool: + # parse bool literal + let v = + try: + parseBool(p.curr.value) + except ValueError: + return nil + result = ast.newBool(v, p.curr) + walk p + +proc parseVarDef(p: var Parser, ident: TokenTuple, varType: TokenKind): Node {.gcsafe.} = + # parse a new variable definition + result = ast.newNode(ntVariableDef, ident) + result.varName = ident.value + result.varImmutable = varType == tkConst + +proc parseDotExpr(p: var Parser, lhs: Node): Node = + # parse dot expression + result = ast.newNode(ntDotExpr, p.prev) + result.lhs = lhs + walk p # tkDot + if likely(p.curr is tkIdentifier): + result.rhs = ast.newIdent(p.curr) + walk p + while p.curr is tkDot: + result = p.parseDotExpr(result) + else: + return nil + +proc parseBracketExpr(p: var Parser, lhs: Node): Node = + # parse bracket expression + result = ast.newNode(ntBracketExpr, p.prev) + +prefixHandle pIdent: + # parse an identifier + result = ast.newIdent(p.curr) + let storageType = p.getStorageType() + walk p + case p.curr.kind + of tkDot: + # handle dot expressions + result = p.parseDotExpr(result) + result.storageType = storageType + return # result + of tkLB: + # handle bracket expressions + result = p.parseBracketExpr(result) + result.storageType = storageType + return # result + else: discard + +proc pIdentOrAssignment(p: var Parser): Node {.gcsafe.} = + let ident = p.curr + if p.next is tkAssign: + walk p, 2 # tkAssign + let varValue = p.getPrefixOrInfix() + caseNotNil varValue: + return ast.newAssignment(ident, varValue) + return p.pIdent() + +prefixHandle pAssignment: + # parse assignment + let ident = p.next + let varType = p.curr.kind + walk p, 2 + expectWalk tkAssign + result = + case p.curr.kind: + of tkAssignableSet: + var varDef = p.parseVarDef(ident, varType) + caseNotNil varDef: + let varValue = p.getPrefixOrInfix() + caseNotNil varValue: + varDef.varValue = varvalue + varDef + else: + nil + +prefixHandle pEchoCommand: + # parse `echo` command + let tk = p.curr + walk p + var varNode: Node + case p.curr.kind + of tkAssignableSet: + if p.curr is tkIdentVar: + if p.next.isInfix: + varNode = p.getPrefixOrInfix() + else: + varNode = p.pIdent() + else: + varNode = p.getPrefixOrInfix() + return ast.newCommand(cmdEcho, varNode, tk) + else: discard + +prefixHandle pReturnCommand: + # parse `return` command + let tk = p.curr + if p.next in tkAssignableSet: + walk p + let varValue = p.getPrefixOrInfix() + return ast.newCommand(cmdReturn, varValue, tk) + +template anyAttrIdent(): untyped = + ( + (p.curr in {tkString, tkIdentifier, tkIf, tkFor, + tkElif, tkElse, tkOr, tkIn} and p.next is tkAssign) or + (p.curr is tkIdentifier and (p.curr.line == el.line or (p.curr.isChild(el) and p.next is tkAssign))) + ) + +proc parseAttributes(p: var Parser, attrs: var HtmlAttributes, el: TokenTuple) {.gcsafe.} = + # parse HTML element attributes + while true: + case p.curr.kind + of tkEOF: break + of tkDot: + let attrKey = "class" + if attrs.hasKey(attrKey): + attrs[attrKey].add(ast.newString(p.next)) + else: + attrs[attrKey] = @[ast.newString(p.next)] + walk p, 2 + of tkID: + let attrKey = "id" + walk p + if not attrs.hasKey(attrKey): + attrs[attrKey] = @[ast.newString(p.curr)] + walk p + else: + errorWithArgs(duplicateAttribute, p.curr, ["id"]) + else: + if anyAttrIdent(): + let attrKey = p.curr + walk p + if p.curr is tkAssign: walk p + if not attrs.hasKey(attrKey.value): + case p.curr.kind + of tkString: + let attrValue = ast.newString(p.curr) + attrs[attrKey.value] = @[attrValue] + walk p + else: + attrs[attrKey.value] = @[] + else: errorWithArgs(duplicateAttribute, attrKey, [attrKey.value]) + else: break + # errorWithArgs(invalidAttribute, p.prev, [p.prev.value]) + +prefixHandle pElement: + # parse HTML Element + let this = p.curr + let tag = htmlTag(this.value) + result = ast.newHtmlElement(tag, this) + walk p + if result.meta[1] != 0: + # set real indentation size + result.meta[1] = p.lvl * 4 + if p.parentNode.len == 0: + p.parentNode.add(result) + else: + if result.meta[0] > p.parentNode[^1].meta[0]: + p.parentNode.add(result) + # parse HTML attributes + case p.curr.kind + of tkDot, tkID: + result.attrs = HtmlAttributes() + p.parseAttributes(result.attrs, this) + of tkIdentifier: + result.attrs = HtmlAttributes() + p.parseAttributes(result.attrs, this) + else: discard + + case p.curr.kind + of tkColon: + walk p + if likely(p.curr in tkAssignableSet): + case tag + of tagStyle: + p.curr.value = multiReplace(p.curr.value, [ + (re"\s+", " "), + (re";(?=\s*})", ""), + ]) + p.curr.value = p.curr.value.replacef(re"(\s+)(\/\*(.*?)\*\/)(\s+)", "$2") + p.curr.value = p.curr.value.replacef(re"(,|:|;|\{|}|\*\/|>) ", "$1") + p.curr.value = p.curr.value.replacef(re"(:| )0\.([0-9]+)(%|em|ex|px|in|cm|mm|pt|pc)", "${1}.${2}${3}") + p.curr.value = p.curr.value.replacef(re"(:| )(\.?)0(%|em|ex|px|in|cm|mm|pt|pc)", "${1}0") + p.curr.value = replacef(p.curr.value, re"(,|:|;|\{|}|\*\/|>) ", "$1") + p.curr.value = p.curr.value.replacef(re" (,|;|\{|}|>)", "$1") + else: discard + let valNode = p.getPrefixOrInfix() + if likely(valNode != nil): + result.nodes.add(valNode) + of tkGT: + # parse inline HTML tags + var node: Node + while p.curr is tkGT: + inc p.lvl + if likely(p.next is tkIdentifier): + walk p + p.curr.pos = p.lvl + node = p.pElement() + caseNotNil node: + if p.curr.kind != tkEOF and p.curr.pos != 0: + if p.curr.line > node.meta[0]: + let currentParent = p.parentNode[^1] + while p.curr.pos > currentParent.meta[2]: + if p.curr.kind == tkEOF: break + var subNode = p.parsePrefix() + caseNotNil subNode: + node.nodes.add(subNode) + # if p.hasError(): break + if p.curr.pos < currentParent.meta[2]: + dec p.lvl, currentParent.meta[2] div p.curr.pos + delete(p.parentNode, p.parentNode.high) + break + result.nodes.add(node) + if p.lvl != 0: + dec p.lvl + return result + else: discard + # parse nested nodes + let currentParent = p.parentNode[^1] + if p.curr.pos > currentParent.meta[2]: + inc p.lvl + while p.curr.pos > currentParent.meta[2]: + if p.curr is tkEOF: break + var subNode = p.parsePrefix() + caseNotNil subNode: + result.nodes.add(subNode) + if p.curr is tkEOF or p.curr.pos == 0: break # prevent division by zero + if p.curr.pos < currentParent.meta[2]: + dec p.lvl + delete(p.parentNode, p.parentNode.high) + break + elif p.curr.pos == currentParent.meta[2]: + dec p.lvl + if p.curr.pos == 0: p.lvl = 0 # reset level + +proc parseCondBranch(p: var Parser, tk: TokenTuple): ConditionBranch {.gcsafe.} = + walk p # `if` or `elif` token + result.expr = p.getPrefixOrInfix() + if p.curr is tkColon: walk p # colon is optional + if likely(result.expr != nil): + while p.curr.isChild(tk): + let node = p.getPrefixOrInfix() + if likely(node != nil): + add result.body, node + if unlikely(result.body.len == 0): + error(badIndentation, p.curr) + +prefixHandle pCondition: + # parse `if`, `elif`, `else` condition statements + var this = p.curr + var elseBody: seq[Node] + let ifbranch = p.parseCondBranch(this) + caseNotNil ifbranch.expr: + result = ast.newCondition(ifbranch, this) + while p.curr is tkElif: + # parse `elif` branches + let eliftk = p.curr + let condBranch = p.parseCondBranch(eliftk) + caseNotNil condBranch.expr: + if unlikely(condBranch.body.len == 0): + return nil + add result.condElifBranch, condBranch + if p.curr is tkElse: + # parse `else` branch, if any + let elsetk = p.curr + if p.next is tkColon: walk p, 2 + while p.curr.isChild(elsetk): + let node = p.getPrefixOrInfix() + caseNotNil node: + result.condElseBranch.add(node) + if unlikely(result.condElseBranch.len == 0): + return nil + +prefixHandle pFor: + # parse `for` statement + let tk = p.curr + walk p + case p.curr.kind + of tkIdentVar: + result = ast.newNode(ntLoopStmt, tk) + # result.loopItem = p.pIdentOrAssignment() + result.loopItem = ast.newNode(ntVariableDef, p.curr) + result.loopItem.varName = p.curr.value + result.loopItem.varImmutable = true + walk p + expectWalk(tkIN) + expect tkIdentVar: + result.loopItems = p.pIdentOrAssignment() + if p.curr is tkColon: walk p + while p.curr.isChild(tk): + let node = p.getPrefixOrInfix() + caseNotNil node: + result.loopBody.add(node) + if unlikely(result.loopBody.len == 0): + error(badIndentation, p.curr) + else: discard + +prefixHandle pAnoObject: + # parse an anonymous object + let anno = ast.newNode(ntObjectStorage, p.curr) + anno.objectItems = newOrderedTable[string, Node]() + walk p # { + while p.curr.isIdent(anyIdent = true, anyStringKey = true) and p.next.kind == tkColon: + let fName = p.curr + if unlikely(p.curr is tkColon): + return nil + else: walk p, 2 + if likely(anno.objectItems.hasKey(fName.value) == false): + var item: Node + case p.curr.kind + of tkLB: + item = p.pAnoArray() + of tkLC: + item = p.pAnoObject() + else: + item = p.getPrefixOrInfix(includes = tkAssignableSet) + if likely(item != nil): + anno.objectItems[fName.value] = item + else: return + else: + errorWithArgs(duplicateField, fName, [fName.value]) + if p.curr is tkComma: + walk p # next k/v pair + if likely(p.curr is tkRC): + walk p + return anno + +prefixHandle pAnoArray: + # parse an anonymous array + let tk = p.curr + walk p # [ + var items: seq[Node] + while p.curr.kind != tkRB: + var item = p.pAssignable() + if likely(item != nil): + add items, item + else: + if p.curr is tkLB: + item = p.pAnoArray() + caseNotNil item: + add items, item + elif p.curr is tkLC: + item = p.pAnoObject() + caseNotNil item: + add items, item + else: return # todo error + if p.curr is tkComma: + walk p + expectWalk tkRB + result = ast.newNode(ntArrayStorage, tk) + result.arrayItems = items + +proc pAssignable(p: var Parser): Node {.gcsafe.} = + case p.curr.kind + of tkLB: p.pAnoArray() + of tkLC: p.pAnoObject() + else: p.getPrefixOrInfix() + +prefixHandle pViewLoader: + # parse `@view` magic call + if p.tpl.getType != ttLayout: + error(invalidViewLoader, p.curr) + elif p.hasLoadedView: + error(duplicateViewLoader, p.curr) + result = ast.newNode(ntViewLoader, p.curr) + p.tpl.setViewIndent(uint(result.meta[1])) + p.hasLoadedView = true + walk p + +prefixHandle pInclude: + # parse `@include` magic call. + # Tim parse included files in separate threads + # using pkg/importer + if likely p.next is tkString: + let tk = p.curr + walk p + result = ast.newNode(ntInclude, tk) + p.includePartial(result, p.curr.value) + walk p + while p.curr is tkComma: + walk p + if likely p.curr is tkString: + p.includePartial(result, p.curr.value) + walk p + else: return nil + # add p.includes, result + +prefixHandle pSnippet: + case p.curr.kind + of tkSnippetJS: + result = ast.newNode(ntJavaScriptSnippet, p.curr) + result.jsCode = p.curr.value + else: discard + # elif p.curr.kind == tkSass: + # result = ast.newSnippet(p.curr) + # result.sassCode = p.curr.value + # elif p.curr.kind in {tkJson, tkYaml}: + # let code = p.curr.value.split(Newlines, maxsplit = 1) + # var ident = code[0] + # p.curr.value = code[1] + # if p.curr.kind == tkJson: + # result = newSnippet(p.curr, ident) + # result.jsonCode = p.curr.value + # else: + # p.curr.kind = tkJson + # result = newSnippet(p.curr, ident) + # # result.jsonCode = yaml(p.curr.value).toJsonStr + walk p + +# +# Infix Main Handlers +# +proc parseMathExp(p: var Parser, lhs: Node): Node {.gcsafe.} +proc parseCompExp(p: var Parser, lhs: Node): Node {.gcsafe.} + +proc parseCompExp(p: var Parser, lhs: Node): Node {.gcsafe.} = + # parse logical expressions with symbols (==, !=, >, >=, <, <=) + let op = getInfixOp(p.curr.kind, false) + walk p + let rhstk = p.curr + let rhs = p.parsePrefix(includes = tkComparable) + if likely(rhs != nil): + result = ast.newNode(ntInfixExpr, rhstk) + result.infixLeft = lhs + result.infixOp = op + if p.curr.kind in tkMathSet: + result.infixRight = p.parseMathExp(rhs) + else: + result.infixRight = rhs + case p.curr.kind + of tkOr, tkOrOr, tkAnd, tkAndAnd: + let infixNode = ast.newNode(ntInfixExpr, p.curr) + infixNode.infixLeft = result + infixNode.infixOp = getInfixOp(p.curr.kind, true) + walk p + let rhs = p.getPrefixOrInfix() + caseNotNil rhs: + infixNode.infixRight = rhs + return infixNode + else: discard + +proc parseMathExp(p: var Parser, lhs: Node): Node {.gcsafe.} = + # parse math expressions with symbols (+, -, *, /) + let infixOp = getInfixMathOp(p.curr.kind, false) + walk p + let rhstk = p.curr + let rhs = p.parsePrefix(includes = tkComparable) + if likely(rhs != nil): + result = ast.newNode(ntMathInfixExpr, rhstk) + result.infixMathOp = infixOp + result.infixMathLeft = lhs + case p.curr.kind + of tkMultiply, tkDivide: + result.infixMathRight = p.parseMathExp(rhs) + of tkPlus, tkMinus: + result.infixMathRight = rhs + result = p.parseMathExp(result) + else: + result.infixMathRight = rhs + +proc getInfixFn(p: var Parser, excludes, includes: set[TokenKind] = {}): InfixFunction {.gcsafe.} = + case p.curr.kind + of tkCompSet: parseCompExp + of tkMathSet: parseMathExp + else: nil + +proc parseInfix(p: var Parser, lhs: Node): Node {.gcsafe.} = + var infixNode: Node # ntInfix + let infixFn = p.getInfixFn() + if likely(infixFn != nil): + result = p.infixFn(lhs) + if p.curr in tkCompSet: + result = p.parseCompExp(result) + +# +# Prefix Main Handlers +# +proc getPrefixFn(p: var Parser, excludes, includes: set[TokenKind] = {}): PrefixFunction {.gcsafe.} = + if excludes.len > 0: + if p.curr in excludes: + errorWithArgs(invalidContext, p.curr, [p.curr.value]) + if includes.len > 0: + if p.curr notin includes: + errorWithArgs(invalidContext, p.curr, [p.curr.value]) + result = + case p.curr.kind + of tkVar, tkConst: pAssignment + of tkString: pString + of tkInteger: pInt + of tkFloat: pFloat + of tkBool: pBool + of tkEchoCmd: pEchoCommand + of tkIF: pCondition + of tkFor: pFor + of tkIdentifier: pElement + of tkIdentVar: pIdent + of tkViewLoader: pViewLoader + of tkSnippetJS: pSnippet + of tkInclude: pInclude + of tkLB: pAnoArray + of tkLC: pAnoObject + else: nil + +proc parsePrefix(p: var Parser, excludes, includes: set[TokenKind] = {}): Node {.gcsafe.} = + let prefixFn = p.getPrefixFn(excludes, includes) + if likely(prefixFn != nil): + return p.prefixFn(excludes, includes) + result = nil + +proc getPrefixOrInfix(p: var Parser, includes, + excludes: set[TokenKind] = {}, infix: Node = nil): Node {.gcsafe.} = + let lhs = p.parsePrefix(excludes, includes) + var infixNode: Node + if p.curr.isInfix: + if likely(lhs != nil): + infixNode = p.parseInfix(lhs) + if likely(infixNode != nil): + return infixNode + else: return + result = lhs + +proc parseRoot(p: var Parser, excludes, includes: set[TokenKind] = {}): Node {.gcsafe.} = + # Parse elements declared at root-level + result = + case p.curr.kind + of tkVar,tkConst: p.pAssignment() + of tkEchoCmd: p.pEchoCommand() + of tkReturnCmd: p.pReturnCommand() + of tkIdentVar: p.pIdentOrAssignment() + of tkIF: p.pCondition() + of tkFor: p.pFor() + of tkViewLoader: p.pViewLoader() + of tkIdentifier: p.pElement() + of tkSnippetJS: p.pSnippet() + of tkInclude: p.pInclude() + of tkLB: p.pAnoArray() + of tkLC: p.pAnoObject() + else: nil + if unlikely(result == nil): + let tk = if p.curr isnot tkEOF: p.curr else: p.prev + errorWithArgs(unexpectedToken, tk, [tk.value]) + +proc newParser*(engine: Tim, tpl: TimTemplate, isMainParser = true): Parser {.gcsafe.} +proc getAst*(p: Parser): Ast {.gcsafe.} + +let partials = PartialTable() +proc parseHandle[T](i: Import[T], importFile: ImportFile, + ticket: ptr TicketLock): seq[string] {.gcsafe, nimcall.} = + withLock ticket[]: + let fpath = importFile.getImportPath + let path = fpath.replace(i.handle.engine.getSourcePath() / $(ttPartial) / "", "") + if likely(not partials.hasKey(path)): + var tpl: TimTemplate = i.handle.engine.getTemplateByPath(fpath) + var childParser: Parser = i.handle.engine.newParser(tpl, false) + partials[path] = childParser.getAst() + +template startParse(path: string): untyped = + p.handle.curr = p.handle.lex.getToken() + p.handle.next = p.handle.lex.getToken() + p.handle.logger = Logger(filePath: path) + while p.handle.curr isnot tkEOF: + if unlikely(p.handle.lex.hasError): + p.handle.logger.newError(internalError, p.handle.curr.line, + p.handle.curr.col, false, p.handle.lex.getError) + if unlikely(p.handle.hasErrors): + echo "error" + break + let node = p.handle.parseRoot() + if likely(node != nil): + add p.handle.tree.nodes, node + lexbase.close(p.handle.lex) + if p.handle.includes.len > 0: + # continue parse other included templates + p.imports(p.handle.includes, parseHandle[Parser]) + +# +# Public API +# +proc newParser*(engine: Tim, tpl: TimTemplate, isMainParser = true): Parser {.gcsafe.} = + ## Parse `tpl` TimTemplate + var p = newImport[Parser](tpl.sources.src, engine.getSourcePath() / $(ttPartial), baseIsMain=true) + p.handle.lex = newLexer(readFile(tpl.sources.src), allowMultilineStrings = true) + p.handle.engine = engine + p.handle.tpl = tpl + p.handle.isMain = isMainParser + startParse(tpl.sources.src) + if isMainParser: + {.gcsafe.}: + p.handle.tree.partials = partials + result = p.handle + +proc getAst*(p: Parser): Ast {.gcsafe.} = + ## Returns the constructed AST + result = p.tree \ No newline at end of file diff --git a/src/tim/engine/tokens.nim b/src/tim/engine/tokens.nim new file mode 100644 index 0000000..0f182f9 --- /dev/null +++ b/src/tim/engine/tokens.nim @@ -0,0 +1,162 @@ +# 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 pkg/toktok + +handlers: + proc handleDocBlock(lex: var Lexer, kind: TokenKind) = + while true: + case lex.buf[lex.bufpos] + of '*': + add lex + if lex.current == '/': + add lex + break + of NewLines: + inc lex.lineNumber + add lex + of EndOfFile: break + else: add lex + lex.kind = kind + + proc handleInlineComment(lex: var Lexer, kind: TokenKind) = + inc lex.bufpos + while true: + case lex.buf[lex.bufpos]: + of NewLines: + lex.handleNewLine() + break + of EndOfFile: break + else: + inc lex.bufpos + lex.kind = kind + + proc handleVar(lex: var Lexer, kind: TokenKind) = + lexReady lex + inc lex.bufpos + case lex.buf[lex.bufpos] + of IdentStartChars: + add lex + while true: + case lex.buf[lex.bufpos] + of IdentChars: + add lex + of Whitespace, EndOfFile: + lex.handleNewLine() + break + else: + break + else: discard + lex.kind = kind + if lex.token.len > 255: + lex.setError("Identifier name is longer than 255 characters") + + proc handleMagics(lex: var Lexer, kind: TokenKind) = + template collectSnippet(tkind: TokenKind) = + while true: + try: + case lex.buf[lex.bufpos] + of EndOfFile: + lex.setError("EOF reached before closing @end") + return + of '@': + if lex.next("end"): + lex.kind = tkind + lex.token = lex.token.unindent(pos + 2) + inc lex.bufpos, 4 + break + else: + add lex + of NewLines: + add lex.token, "\n" + lex.handleNewLine() + else: + add lex + except: + lex.bufpos = lex.handleRefillChar(lex.bufpos) + lexReady lex + if lex.next("js"): + let pos = lex.getColNumber(lex.bufpos) + inc lex.bufpos, 3 + collectSnippet(tkSnippetJS) + elif lex.next("include"): + lex.setToken tkInclude, 8 + elif lex.next("view"): + lex.setToken tkViewLoader, 5 + else: discard + + +const toktokSettings = + toktok.Settings( + tkPrefix: "tk", + lexerName: "Lexer", + lexerTuple: "TokenTuple", + lexerTokenKind: "TokenKind", + tkModifier: defaultTokenModifier, + useDefaultIdent: true, + keepUnknown: true, + keepChar: true, + ) + +registerTokens toktokSettings: + plus = '+' + minus = '-' + multiply = '*' + divide = '/': + doc = tokenize(handleDocBlock, '*') + comment = tokenize(handleInlineComment, '/') + `mod` = '%' + lc = '{' + rc = '}' + lp = '(' + rp = ')' + lb = '[' + rb = ']' + dot = '.' + id = '#' + exc = '!': + ne = '=' + assign = '=': + eq = '=' + colon = ':' + comma = ',' + gt = '>': + gte = '=' + lt = '<': + lte = '=' + amp = '&': + andAnd = '&' + pipe = '|': + orOr = '|' + `if` = "if" + `elif` = "elif" + `else` = "else" + `and` = "and" + `for` = "for" + `in` = "in" + `or` = "or" + `bool` = ["true", "false"] + + # literals + litBool = "bool" + litInt = "int" + litString = "string" + litFloat = "float" + litObject = "object" + litArray = "array" + + # magics + at = tokenize(handleMagics, '@') + snippetjs + viewLoader + `include` + + fn = "fn" + `var` = "var" + `const` = "const" + returnCmd = "return" + echoCmd = "echo" + identVar = tokenize(handleVar, '$') \ No newline at end of file diff --git a/src/timpkg/commands/buildCommand.nim b/src/timpkg/commands/buildCommand.nim deleted file mode 100644 index 91fea60..0000000 --- a/src/timpkg/commands/buildCommand.nim +++ /dev/null @@ -1,11 +0,0 @@ -# A high-performance template engine & markup language -# inspired by the Emmet syntax. -# -# (c) 2023 George Lemon | MIT License -# Made by Humans from OpenPeeps -# https://github.com/openpeeps/tim - -import kapsis/runtime - -proc runCommand*(v: Values) = - discard # todo \ No newline at end of file diff --git a/src/timpkg/commands/initCommand.nim b/src/timpkg/commands/initCommand.nim deleted file mode 100644 index 91fea60..0000000 --- a/src/timpkg/commands/initCommand.nim +++ /dev/null @@ -1,11 +0,0 @@ -# A high-performance template engine & markup language -# inspired by the Emmet syntax. -# -# (c) 2023 George Lemon | MIT License -# Made by Humans from OpenPeeps -# https://github.com/openpeeps/tim - -import kapsis/runtime - -proc runCommand*(v: Values) = - discard # todo \ No newline at end of file diff --git a/src/timpkg/commands/setupEngine.nim b/src/timpkg/commands/setupEngine.nim deleted file mode 100644 index 05af8ee..0000000 --- a/src/timpkg/commands/setupEngine.nim +++ /dev/null @@ -1,73 +0,0 @@ -# A high-performance template engine & markup language -# inspired by the Emmet syntax. -# -# (c) 2023 George Lemon | MIT License -# Made by Humans from OpenPeeps -# https://github.com/openpeeps/tim - -import std/tables -import pkg/[watchout, klymene/cli] -import timpkg/engine/[ast, parser, meta, compiler/transpiler] - -from std/os import getCurrentDir -from std/times import cpuTime - -const DockType = "" - -var Tim*: TimEngine -const DefaultLayout = "base" - -proc compileCode(engine: TimEngine, t: var TimlTemplate) = - var p = engine.parse(t.getSourceCode(), t.getFilePath(), templateType = t.getType()) - if p.hasError(): - display p.getError() - return - var c = newCompiler(p.getStatements, t, engine.shouldMinify, - engine.getIndent, t.getFilePath) - if c.hasError(): - display t.getFilePath - for err in c.getErrors(): - display err - -proc precompile*(engine: var TimEngine, callback: proc() {.gcsafe, nimcall.} = nil, - debug = false): seq[string] {.discardable.} = - ## Pre-compile ``views`` and ``layouts`` - ## from ``.timl`` to HTML or BSON. - ## - ## Note that ``partials`` contents are collected on - ## compile-time and merged within the view. - if Tim.hasAnySources: - # Will use `watchout` to watch for changes in `/templates` dir - display "✨ Watching for changes..." - proc watchoutCallback(file: watchout.File) {.closure.} = - let initTime = cpuTime() - display "✨ Changes detected" - display file.getName(), indent = 3 - var timlTemplate = getTemplateByPath(Tim, file.getPath()) - if timlTemplate.isPartial: - for depView in timlTemplate.getDependentViews(): - Tim.compileCode(getTemplateByPath(Tim, depView)) - else: - Tim.compileCode(timlTemplate) - display("Done in " & $(cpuTime() - initTime), indent = 2) - if callback != nil: # Run a custom callback, if available - callback() - - var watchFiles: seq[string] - for id, view in Tim.getViews().mpairs(): - Tim.compileCode(view) - watchFiles.add view.getFilePath() - result.add view.getName() - - for id, partial in Tim.getPartials().pairs(): - # Watch for changes in `partials` directory. - watchFiles.add partial.getFilePath() - - for id, layout in Tim.getLayouts().mpairs(): - Tim.compileCode(layout) - watchFiles.add layout.getFilePath() - result.add layout.getName() - - # Start a new Thread with Watchout watching for live changes - startThread(watchoutCallback, watchFiles, 450, shouldJoinThread = true) - else: display("Can't find views") \ No newline at end of file diff --git a/src/timpkg/commands/watchCommand.nim b/src/timpkg/commands/watchCommand.nim deleted file mode 100644 index 8fe0b14..0000000 --- a/src/timpkg/commands/watchCommand.nim +++ /dev/null @@ -1,11 +0,0 @@ -# A high-performance template engine & markup language -# inspired by the Emmet syntax. -# -# (c) 2023 George Lemon | MIT License -# Made by Humans from OpenPeeps -# https://github.com/openpeeps/tim - -import kapsis/runtime - -proc runCommand*(v: Values) = - discard \ No newline at end of file diff --git a/src/timpkg/commands/xmlCommand.nim b/src/timpkg/commands/xmlCommand.nim deleted file mode 100644 index 91fea60..0000000 --- a/src/timpkg/commands/xmlCommand.nim +++ /dev/null @@ -1,11 +0,0 @@ -# A high-performance template engine & markup language -# inspired by the Emmet syntax. -# -# (c) 2023 George Lemon | MIT License -# Made by Humans from OpenPeeps -# https://github.com/openpeeps/tim - -import kapsis/runtime - -proc runCommand*(v: Values) = - discard # todo \ No newline at end of file diff --git a/src/timpkg/engine/ast.nim b/src/timpkg/engine/ast.nim deleted file mode 100644 index 69cd656..0000000 --- a/src/timpkg/engine/ast.nim +++ /dev/null @@ -1,302 +0,0 @@ -# A blazing fast, cross-platform, multi-language -# template engine and markup language written in Nim. -# -# Made by Humans from OpenPeeps -# (c) George Lemon | LGPLv3 License -# https://github.com/openpeeps/tim - -import std/[tables, json, jsonutils] - -from ./tokens import TokenKind, TokenTuple -from std/enumutils import symbolName - -type - NodeType* = enum - ntNone = "none" - ntStmtList = "StatementList" - ntInt = "int" - ntFloat = "float" - ntString = "string" - ntBool = "bool" - ntId = "ident" - ntHtmlElement = "HtmlElement" - ntStatement - ntVarExpr = "VariableDeclaration" - ntCondition = "ConditionStatement" - ntShortConditionStmt = "ShortConditionStatement" - ntForStmt = "ForStatement" - ntMixinStmt = "MixinStatement" - ntInfixStmt - ntIncludeCall - ntCall - ntMixinCall - ntMixinDef - ntLet - ntVar - ntVariable - ntIdentifier - ntView - ntJavaScript - ntSass - ntJson - ntYaml - ntResult - ntRuntime - - InfixOp* {.pure.} = enum - None = "none" - EQ = "==" - NEQ = "!=" - GT = ">" - GTE = ">=" - LT = "<" - LTE = "<=" - AND = "and" - AMP = "&" # string concatenation - - HtmlAttributes* = Table[string, seq[Node]] - IfBranch* = tuple[cond: Node, body: seq[Node]] - SIfBranch* = tuple[cond: Node, body: HtmlAttributes] - ElifBranch* = seq[IfBranch] - MetaNode* = tuple[line, pos, col, wsno: int] - ParamTuple* = tuple[key, value, typeSymbol: string, `type`: NodeType] - - AccessorKind* {.pure.} = enum - None, Key, Value - - VarVisibility* = enum - GlobalVar - ScopeVar - InternalVar - - Node* {.acyclic.} = ref object - case nt*: NodeType - of ntInt: - iVal*: int - of ntFloat: - fVal*: float - of ntString: - sVal*: string - sConcat*: seq[Node] - of ntBool: - bVal*: bool - of ntId: - idVal*: string - of ntCondition: - ifCond*: Node - ifBody*, elseBody*: seq[Node] - elifBranch*: ElifBranch - of ntShortConditionStmt: - sIfCond*: Node - sIfBody*: HtmlAttributes - of ntForStmt: - forItem*: Node # ntVariable - forItems*: Node # ntVariable - forBody*: seq[Node] - of ntHtmlElement: - htmlNodeName*: string - attrs*: HtmlAttributes - nodes*: seq[Node] - selfCloser*: bool - of ntStmtList: - stmtList*: Node - of ntInfixStmt: - infixOp*: InfixOp - infixLeft*, infixRight*: Node - of ntIncludeCall: - includeIdent*: string - of ntCall: - callIdent*: string - callParams*: seq[Node] # ntString or ntVariable - of ntMixinCall: - mixinIdent*: string - of ntMixinDef: - mixinIdentDef*: string - mixinParamsDef*: seq[ParamTuple] - mixinBody*: seq[Node] - of ntVarExpr: - varIdentExpr*: string - varTypeExpr*: NodeType # ntBool, ntInt, ntString, ntFloat - varValue*: Node - of ntVariable: # todo rename ntVarCall - varIdent*: string - varSymbol*: string - varType*: NodeType # ntBool, ntInt, ntString, ntFloat - visibility*: VarVisibility - isSafeVar*: bool - dataStorage*: bool - accessors*: seq[Node] - case accessorKind*: AccessorKind - of Key: - byKey*: string - else: discard - of ntJavaScript: - jsCode*: string - of ntSass: - sassCode*: string - of ntJson: - jsonIdent*, jsonCode*: string - of ntYaml: - yamlCode*: string - of ntRuntime: - runtimeIdent*, runtimeCode*: string - else: discard - meta*: MetaNode - - Tree* = object - nodes*: seq[Node] - -proc `$`*(node: Node): string = - result = pretty(toJson(node)) - -proc `$`*(tree: Tree): string = - result = pretty(toJson(tree)) - -proc newNode*(nt: NodeType, tk: TokenTuple): Node = - ## Create a new Node - result = Node(nt: nt) - result.meta = (tk.line, tk.pos, tk.col, tk.wsno) - -proc newSnippet*(tk: TokenTuple, ident = ""): Node = - ## Add a new Snippet node. It can be `ntJavaScript`, - ## `ntSass`, `ntJSON` or `ntYaml` - if tk.kind == tkJS: - result = newNode(ntJavaScript, tk) - elif tk.kind == tkSASS: - result = newNode(ntSass, tk) - elif tk.kind == tkJSON: - result = newNode(ntJSon, tk) - result.jsonIdent = ident - elif tk.kind == tkYAML: - result = newNode(ntYaml, tk) - -proc newExpression*(expression: Node): Node = - ## Add a new `ntStmtList` expression node - result = Node( - nt: ntStmtList, - stmtList: expression - ) - -proc newInfix*(infixLeft, infixRight: Node, infixOp: InfixOp): Node = - ## Add a new `ntInfixStmt` node - Node( - nt: ntInfixStmt, - infixLeft: infixLeft, - infixRight: infixRight, - infixOp: infixOp, - ) - -proc newVar*(tk: TokenTuple, varType: NodeType, varValue: Node): Node = - result = newNode(ntVarExpr, tk) - result.varIdentExpr = tk.value - result.varTypeExpr = varType - result.varValue = varValue - -proc newInfix*(infixLeft: Node): Node = - ## Add a new `ntInfixStmt` node - Node(nt: ntInfixStmt, infixLeft: infixLeft) - -proc newInt*(iVal: int, tk: TokenTuple): Node = - ## Add a new `ntInt` node - Node(nt: ntInt, iVal: iVal, meta: (tk.line, tk.pos, tk.col, tk.wsno)) - -proc newBool*(bVal: bool): Node = - ## Add a new `ntBool` node - Node(nt: ntBool, bVal: bVal) - -proc newFloat*(fVal: float): Node = - ## Add a new `ntFloat` node - Node(nt: ntFloat, fVal: fVal) - -proc newString*(tk: TokenTuple, strs: seq[Node] = @[]): Node = - ## Add a new `ntString` node - Node( - nt: ntString, - sVal: tk.value, - sConcat: strs, - meta: (tk.line, tk.pos, tk.col, tk.wsno) - ) - -proc newHtmlElement*(tk: TokenTuple): Node = - ## Add a new `ntHtmlElement` node - Node( - nt: ntHtmlElement, - htmlNodeName: tk.value, - meta: (tk.line, tk.pos, tk.col, tk.wsno) - ) - -proc newIfExpression*(ifBranch: IfBranch, tk: TokenTuple): Node = - ## Add a mew Conditional node - Node( - nt: ntCondition, - ifCond: ifBranch.cond, - ifBody: ifBranch.body, - meta: (tk.line, tk.pos, tk.col, tk.wsno) - ) - -proc newShortIfExpression*(ifBranch: SIfBranch, tk: TokenTuple): Node = - ## Add a new short hand conditional node - Node( - nt: ntShortConditionStmt, - sIfCond: ifBranch.cond, - sIfBody: ifBranch.body, - meta: (tk.line, tk.pos, tk.col, tk.wsno) - ) - -proc newCall*(ident: string, params: seq[Node]): Node = - ## Add a new `ntCall` node - Node(nt: ntCall, callIdent: ident, callParams: params) - -proc newMixin*(tk: TokenTuple): Node = - ## Add a new `ntMixinCall` node - Node(nt: ntMixinCall, mixinIdent: tk.value) - -proc newMixinDef*(tk: TokenTuple): Node = - Node(nt: ntMixinDef, mixinIdentDef: tk.value) - -proc newView*(tk: TokenTuple): Node = - Node(nt: ntView, meta: (tk.line, tk.pos, tk.col, tk.wsno)) - -proc newInclude*(ident: string): Node = - ## Add a new `ntIncludeCall` node - Node(nt: ntIncludeCall, includeIdent: ident) - -proc newFor*(itemVarIdent, itemsVarIdent: Node, body: seq[Node], tk: TokenTuple): Node = - ## Add a new `ntForStmt` node - result = newNode(ntForStmt, tk) - result.forBody = body - result.forItem = itemVarIdent - result.forItems = itemsVarIdent - -proc newVariable*(tk: TokenTuple, isSafeVar, dataStorage = false, - varType = ntString, varVisibility: VarVisibility = GlobalVar): Node = - ## Add a new `ntVariable` node - result = newNode(ntVariable, tk) - result.varIdent = tk.value - result.varSymbol = "$" & tk.value - result.isSafeVar = isSafeVar - result.dataStorage = dataStorage - result.varType = varType - result.visibility = varVisibility - -proc newVarCallKeyAccessor*(tk: TokenTuple, fid: string): Node = - result = Node( - nt: ntVariable, - accessorKind: Key, - byKey: fid, - varIdent: tk.value, - varSymbol: "$" & tk.value, - meta: (tk.line, tk.pos, tk.col, tk.wsno) - ) - -proc newVarCallValAccessor*(tk: TokenTuple): Node = - result = Node( - nt: ntVariable, - accessorKind: Value, - varIdent: tk.value, - varSymbol: "$" & tk.value, - meta: (tk.line, tk.pos, tk.col, tk.wsno) - ) - -proc newRuntime*(tk: TokenTuple): Node = - result = Node(nt: ntRuntime, runtimeCode: tk.value, runtimeIdent: tk.attr[0]) \ No newline at end of file diff --git a/src/timpkg/engine/compiler.nim b/src/timpkg/engine/compiler.nim deleted file mode 100644 index f421085..0000000 --- a/src/timpkg/engine/compiler.nim +++ /dev/null @@ -1,389 +0,0 @@ -# A blazing fast, cross-platform, multi-language -# template engine and markup language written in Nim. -# -# Made by Humans from OpenPeeps -# (c) George Lemon | LGPLv3 License -# https://github.com/openpeeps/tim - -import std/[tables, critbits, strutils, json] - -import ./ast -from ./meta import Tim, Template, TemplateType, getType - -type - ScopeTable = TableRef[string, Node] - HtmlCompiler* = ref object - ast: Tree - tpl: Template - case templateType: TemplateType - of ttLayout: - head: string - else: discard - minify: bool - indent: int - html, js, sass, json, - yaml, runtime: string - hasJs, hasSass, hasJson, - hasYaml, hasRuntime: bool - output: string - error: string - nl: string = "\n" - stickytail: bool - ## if false inserts a \n line before closing - ## the element. This does not apply - ## to `textarea`, `submit` `button` and self closing tags. - when defined timStandalone: - discard - else: - engine: Tim - data: JsonNode - globalScope: ScopeTable = ScopeTable() - -# -# Forward declaration -# -proc writeInnerNode(c: HtmlCompiler, nodes: seq[Node], scopetables: var seq[ScopeTable]) -proc writeNode(c: HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) - -proc hasError*(c: HtmlCompiler): bool = - result = c.error.len > 0 - -proc getError*(c: HtmlCompiler): string = - result = c.error - -when not defined timStandalone: - # Available when Tim is imported as a Nim library. - # If you want native performance, you can switch - # to Tim CLI and transpile `.timl` templates to static `.nim` files - - # - # Scope API - # - proc globalScope(c: HtmlCompiler, node: Node) = - # Add `node` to global scope - c.globalScope[node.varIdentExpr] = node - - proc `+=`(scope: ScopeTable, node: Node) = - # Add `node` to current `scope` - scope[node.varIdentExpr] = node - - proc stack(c: HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = - # Add `node` to either local or global scope - if scopetables.len > 0: - scopetables[^1] += node - return - c.globalScope += node - - proc getCurrentScope(c: HtmlCompiler, scopetables: var seq[ScopeTable]): ScopeTable = - # Returns the current `ScopeTable`. When not found, - # returns the `globalScope` ScopeTable - if scopetables.len > 0: - return scopetables[^1] # the last scope - return c.globalScope - - proc getScope(c: HtmlCompiler, key: string, - scopetables: var seq[var ScopeTable]): tuple[scopeTable: ScopeTable, index: int] = - # Walks (bottom-top) through available `scopetables`, and finds - # the closest `ScopeTable` that contains a node for given `key`. - # If found returns the ScopeTable followed by index (position). - for i in countdown(scopetables.high, scopetables.low): - if scopetables[i].hasKey(key): - return (scopetables[i], i) - if likely c.globalScope.hasKey(key): - result = (c.globalScope, 0) - - proc inScope(key: string, scopetables: var seq[ScopeTable]): bool = - # Performs a quick search in the current `ScopeTable` - if scopetables.len > 0: - result = scopetables[^1].hasKey(key) - - proc fromScope(c: HtmlCompiler, key: string, scopetables: var seq[ScopeTable]): Node = - # Retrieves a node with given `key` from `scopetables` - let some = c.getScope(key, scopetables) - if some.scopeTable != nil: - result = some.scopeTable[key] - - # - # AST Evaluators for JIT computation - # - proc getValue(c: HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]): Node - proc writeValue(c: HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) - - proc infixEvaluator(c: HtmlCompiler, lhs, rhs: Node, infixOp: InfixOp, scopetables: var seq[ScopeTable]): bool = - # Evaluates `a` with `b` based on given infix operator - case infixOp: - of EQ: - case lhs.nt: - of ntBool: - case rhs.nt - of ntBool: - result = lhs.bVal == rhs.bVal - else: discard - of ntString: - case rhs.nt - of ntString: - result = lhs.sVal == rhs.sVal - else: discard - of ntInt: - case rhs.nt - of ntInt: - result = lhs.iVal == rhs.iVal - of ntFloat: - result = toFloat(lhs.iVal) == rhs.fVal - else: discard - of ntFloat: - case rhs.nt - of ntFloat: - result = lhs.fVal == rhs.fVal - of ntInt: - result = lhs.fVal == toFloat(rhs.iVal) - else: discard - else: discard - of GT: - case lhs.nt: - of ntInt: - case rhs.nt - of ntInt: - result = lhs.iVal > rhs.iVal - of ntFloat: - result = toFloat(lhs.iVal) > rhs.fVal - else: discard - of ntFloat: - case rhs.nt - of ntFloat: - result = lhs.fVal > rhs.fVal - of ntInt: - result = lhs.fVal > toFloat(rhs.iVal) - else: discard - else: discard # handle float - of GTE: - case lhs.nt: - of ntInt: - case rhs.nt - of ntInt: - result = lhs.iVal >= rhs.iVal - of ntFloat: - result = toFloat(lhs.iVal) >= rhs.fVal - else: discard - of ntFloat: - case rhs.nt - of ntFloat: - result = lhs.fVal >= rhs.fVal - of ntInt: - result = lhs.fVal >= toFloat(rhs.iVal) - else: discard - else: discard # handle float - else: discard # todo - - proc evalCondition(c: HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = - # Evaluates an `if`, `elif`, `else` conditional node - if c.infixEvaluator(node.ifCond.infixLeft, - node.ifCond.infixRight, node.ifCond.infixOp, scopetables): - c.writeInnerNode(node.ifBody, scopetables) - return # condition is truthy - - # handle `elif` branches - if node.elifBranch.len > 0: - for elifBranch in node.elifBranch: - if c.infixEvaluator(elifBranch.cond.infixLeft, - elifBranch.cond.infixRight, elifBranch.cond.infixOp, scopetables): - c.writeInnerNode(elifBranch.body, scopetables) - return # condition is truthy - - # handle `else` branch - if node.elseBody.len > 0: - c.writeInnerNode(node.elseBody, scopetables) - - proc evalFor(c: HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = - # Evaluates a `for` node - discard - - proc evalVar(c: HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]): Node = - # Evaluates a variable call - let varNode = c.fromScope(node.varIdent, scopetables) - if likely(varNode != nil): - return varNode - # todo error, variable not found - - proc getValue(c: HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]): Node = - case node.nt - of ntVariable: - let varNode = c.evalVar(node, scopetables) - else: discard - - proc writeValue(c: HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = - case node.nt - of ntVariable: - let varNode = c.evalVar(node, scopetables) - if likely(varNode != nil): - case varNode.varValue.nt - of ntString: - add c.output, varNode.varValue.sVal - of ntInt: - add c.output, $(varNode.varValue.iVal) - of ntFloat: - add c.output, $(varNode.varValue.fVal) - of ntBool: - add c.output, $(varNode.varValue.bVal) - else: - echo "error" - else: discard - c.stickytail = true - -proc getId(c: HtmlCompiler, node: Node): string = - add result, indent("id=", 1) & "\"" - let attrNode = node.attrs["id"][0] - case attrNode.nt - of ntString: - add result, attrNode.sVal - else: discard # todo - add result, "\"" - -proc getAttrs(c: HtmlCompiler, attrs: HtmlAttributes): string = - var i = 0 - var skipQuote: bool - let len = attrs.len - for k, attrNodes in attrs: - var attrStr: seq[string] - add result, indent("$1=" % [k], 1) & "\"" - for attrNode in attrNodes: - case attrNode.nt - of ntString: - if attrNode.sConcat.len == 0: - add attrStr, attrNode.sVal - else: discard - else: discard - add result, attrStr.join(" ") - if not skipQuote and i != len: - add result, "\"" - else: - skipQuote = false - inc i - -proc baseIndent(c: HtmlCompiler, isize: int): int = - if c.indent == 2: - int(isize / c.indent) - else: - isize - -proc getIndent(c: HtmlCompiler, meta: MetaNode, skipbr = false): string = - case meta.pos - of 0: - if not c.stickytail: - add result, c.nl - else: - if not c.stickytail: - add result, c.nl - add result, indent("", c.baseIndent(meta.pos)) - -template htmlblock(tag: string, body: untyped) = - var isSelfcloser: bool - case c.minify: - of false: - if c.stickytail == true: - c.stickytail = false - add c.output, c.getIndent(node.meta) - else: discard - add c.output, "<" & tag - case node.nt - of ntStmtList: - isSelfcloser = node.stmtList.selfCloser - if node.stmtList.attrs.hasKey("id"): - add c.output, c.getId(node.stmtList) - node.stmtList.attrs.del("id") - if node.stmtList.attrs.len > 0: - add c.output, c.getAttrs(node.stmtList.attrs) - else: - isSelfcloser = node.selfCloser - if node.attrs.hasKey("id"): - add c.output, c.getId(node) - node.attrs.del("id") - if node.attrs.len > 0: - add c.output, c.getAttrs(node.attrs) - add c.output, ">" - body - case isSelfcloser - of false: - case c.minify: - of false: - add c.output, c.getIndent(node.meta) - else: discard - add c.output, "" - c.stickytail = false - else: discard - -proc writeInnerNode(c: HtmlCompiler, nodes: seq[Node], scopetables: var seq[ScopeTable]) = - for node in nodes: - case node.nt: - of ntHtmlElement: - let tag = node.htmlNodeName - htmlblock tag: - if node.nodes.len > 0: - c.writeInnerNode(node.nodes, scopetables) - of ntString: - add c.output, node.sVal - c.stickytail = true - of ntVariable: - c.writeValue(node, scopetables) - of ntCondition: - c.evalCondition(node, scopetables) - of ntView: - add c.output, c.getIndent(node.meta) - c.head = c.output - reset(c.output) - else: discard - -proc writeNode(c: HtmlCompiler, node: Node, scopetables: var seq[ScopeTable]) = - case node.stmtList.nt: - of ntHtmlElement: - let tag = node.stmtList.htmlNodeName - htmlblock tag: - if node.stmtList.nodes.len > 0: - c.writeInnerNode(node.stmtList.nodes, scopetables) - of ntCondition: - c.evalCondition(node.stmtList, scopetables) - of ntForStmt: - c.evalFor(node.stmtList, scopetables) - of ntVarExpr: - if likely(not c.globalScope.hasKey(node.stmtList.varIdentExpr)): - c.globalScope += node.stmtList - of ntView: - # echo node - discard - else: discard - -# -# Public API -# -proc newHtmlCompiler*(ast: Tree, minify: bool, - indent: range[2..4], tpl: Template): HtmlCompiler = - ## Creates a new instance of `HtmlCompiler` - result = HtmlCompiler(ast: ast, minify: minify, - indent: indent, tpl: tpl, templateType: tpl.getType) - if minify: setLen(result.nl, 0) - var scopetables = newSeq[ScopeTable]() - for i in 0 .. result.ast.nodes.high: - result.writeNode(result.ast.nodes[i], scopetables) - -proc getHtml*(c: var HtmlCompiler): string = - case c.minify: - of true: - result = c.output - else: - if c.output.len > 0: - result = c.output[1..^1] - -proc getHead*(c: var HtmlCompiler): string = - ## Returns the top of a split layout - assert c.templateType == ttLayout - case c.minify - of true: - result = c.head - else: - if c.head.len > 0: - result = c.head[1..^1] - -proc getTail*(c: var HtmlCompiler): string = - ## Returns the tail of a layout - assert c.templateType == ttLayout - return c.getHtml() \ No newline at end of file diff --git a/src/timpkg/engine/data.nim b/src/timpkg/engine/data.nim deleted file mode 100644 index 9f0bbf0..0000000 --- a/src/timpkg/engine/data.nim +++ /dev/null @@ -1,197 +0,0 @@ -# A high-performance compiled template engine -# inspired by the Emmet syntax. -# -# (c) 2023 George Lemon | MIT License -# Made by Humans from OpenPeeps -# https://github.com/openpeeps/tim - -import std/[macros, tables, jsonutils] - -from std/strutils import spaces -from std/json import `$` - -{.experimental: "dynamicBindSym".} - -type - TValue* = enum - tValNil - tValBool - tValFloat - tValString - tValInt - tValArray - tValObject - - DataTable = TableRef[string, Value] - - Value = ref object - case vtype: TValue: - of tValBool: - bVal: bool - of tValFloat: - fVal: float - of tValString: - sVal: string - of tValInt: - iVal: int - of tValArray: - arrayValues: seq[Value] - of tValObject: - objectValues: DataTable - else: discard - - Global {.acyclic.} = ref object - data: DataTable - - Scope {.acyclic.} = ref object - data: DataTable - - Local {.acyclic.} = ref object - data: DataTable - -proc assign(value: var NimNode, key, v: NimNode) {.compileTime.} = - case v.kind - of nnkStrLit: - value.add( - newColonExpr(ident "vtype", ident "tValString"), - newColonExpr(ident "sVal", v) - ) - of nnkIntLit: - value.add( - newColonExpr(ident "vtype", ident "tValInt"), - newColonExpr(ident "iVal", v) - ) - of nnkIdent: - if v.strVal in ["true", "false"]: - value.add( - newColonExpr(ident "vtype", ident "tValBool"), - newColonExpr(ident "bVal", v) - ) - else: - let varImpl = v.bindSym.getImpl - expectKind varImpl, nnkIdentDefs - value.assign(key, varImpl[^1]) - of nnkBracket, nnkPrefix: - var list: NimNode - if v.kind == nnkPrefix: - if not eqIdent(v[0], "@"): - error("Expected a sequence or array") - else: list = v[1] - else: - list = v - var items = nnkBracket.newTree() - for l in list: - var arrItem = nnkObjConstr.newTree(ident "Value") - arrItem.assign(key, l) - items.add arrItem - value.add( - newColonExpr(ident "vtype", ident "tValArray"), - newColonExpr( - ident "arrayValues", - nnkPrefix.newTree( - ident "@", - items - ) - ) - ) - echo value.repr - of nnkTableConstr: - echo "X" - else: error("Invalid type for fast object instantiation") - -macro `%*`(fields: untyped): untyped = - expectKind fields, nnkTableConstr - var dataTable = nnkVarSection.newTree( - nnkIdentDefs.newTree( - ident "dataTable", - newEmptyNode(), - newCall( - ident("DataTable") - ) - ) - ) - var dataConstr = nnkObjConstr.newTree( - ident "Global", - newColonExpr( - ident "data", - ident "dataTable" - ) - ) - result = newStmtList() - var dataTableFields = newStmtList() - for f in fields: - expectKind f, nnkExprColonExpr - var value = nnkObjConstr.newTree(ident "Value") - value.assign(f[0], f[1]) - dataTableFields.add( - newAssignment( - nnkBracketExpr.newTree( - ident "dataTable", - f[0] - ), - value - ) - ) - result.add(dataTable) - result.add(dataTableFields) - result.add(dataConstr) - -# -# Runtime API -# -iterator items*(v: Value): Value = - if v.vtype == tValArray: - for k, i in v.arrayValues: - yield i - -proc `$`*(v: Value): string = - ## Returns string representation of Value - case v.vtype: - of tValString: - result = v.sVal - of tValInt: - result = $v.iVal - of tValBool: - result = $v.bVal - of tValFloat: - result = $v.fVal - of tValArray: - add result, "[" - var i = 1 - var arrlen = v.arrayValues.len - for item in v.arrayValues: - add result, "\"" & $(item) & "\"" - if i != arrlen: - add result, "," & spaces(1) - inc i - add result, "]" - of tValObject: - add result, "{" - for o in v.objectValues.pairs(): - add result, $(o) - # $(toJson(v.objectValues)) - add result, "}" - of tValNil: - result = "null" - -var x = "yey" -var t = %*{ - "test": "ok", - "asa": 123, - "asdsa": x, - "exists": true, - "fruits": @["apple", "pineapple"], - "ya": { - "hey": "aaa" - } -} - -# test -echo t.data.len -echo t.data["test"].sVal -echo t.data["asa"].iVal -echo t.data["asdsa"].sVal - -echo t.data["fruits"] -for item in t.data["fruits"].items(): - echo item.sVal \ No newline at end of file diff --git a/src/timpkg/engine/init.nim b/src/timpkg/engine/init.nim deleted file mode 100644 index 10f70d2..0000000 --- a/src/timpkg/engine/init.nim +++ /dev/null @@ -1,218 +0,0 @@ -# A high-performance template engine & markup language -# inspired by the Emmet syntax. -# -# (c) 2023 George Lemon | MIT License -# Made by Humans from OpenPeeps -# https://github.com/openpeeps/tim - -import std/[tables, strutils, json] -import pkg/[pkginfo, jsony, kapsis/cli] -import ./meta, ./ast, ./parser, ./compiler, ./utils - -export parser -export meta except TimEngine - -const DockType = "" -var Tim*: TimEngine -const DefaultLayout = "base" - -when defined cli: - include ./cli/init -else: - when requires "watchout": - import watchout - from std/times import cpuTime - - var reloadHandler: string - when requires "supranim": - when not defined release: - reloadHandler = "\n" & """ - - """ - - proc newJIT(e: TimEngine, tpl: Template, data: JsonNode, - viewCode = "", hasViewCode = false): Compiler = - result = newCompiler( - e = e, - p = e.readAst(tpl), - tpl = tpl, - minify = e.shouldMinify, - indent = e.getIndent, - filePath = tpl.getFilePath, - data = data, - viewCode = viewCode, - hasViewCode = hasViewCode - ) - - proc render*(e: TimEngine, viewName: string, layoutName = DefaultLayout, - data, globals = %*{}): string = - ## Render a template view by name (without extension). Use dot notation - ## to render a nested template render("checkout.loggedin") - if e.hasView viewName: - let layoutName = - if e.hasLayout(layoutName): - layoutName - else: DefaultLayout - var - allData = newJObject() - view: Template = e.getView viewName - layout: Template = e.getLayout layoutName - if e.globalDataExists: - allData.merge("globals", e.getGlobalData, globals) - else: - allData.merge("globals", %*{}, globals) - allData.add("scope", data) - result = DockType - if view.isJitEnabled: - # Compile view at runtime - var cview = newJIT(e, view, allData) - var clayout = newJIT(e, layout, allData, cview.getHtml & reloadHandler, hasViewCode = true) - add result, clayout.getHtml - if clayout.hasError: - for err in clayout.getErrors: - display(span("Warning", fgYellow), span(err)) - display(indent(layout.getFilePath, 1), br="after") - if cview.hasError: - for err in cview.getErrors: - display(span("Warning", fgYellow), span(err)) - display(indent(view.getFilePath, 1), br="after") - freem(cview) - freem(clayout) - else: - if layout.isJitEnabled: - # Compile layout at runtime - var c = newJIT(e, layout, allData, view.getHtmlCode & reloadHandler, hasViewCode = true) - add result, c.getHtml - if c.hasError: - display("Warning:" & indent(layout.getFilePath, 1), br = "before") - for err in c.getErrors: - display(err, indent = 2) - freem(c) - else: - # Otherwise get the precompiled HTML layout from memory - # and resolve the `@view` placeholder using the current - add result, layout.getHtmlCode % [ - layout.getPlaceholderId, - if e.shouldMinify: - view.getHtmlCode & reloadHandler - else: - indent(view.getHtmlCode & reloadHandler, layout.getPlaceholderIndent) - ] - - proc compileCode(e: TimEngine, t: Template, fModified = false) = - # if not t.isModified and fModified == false: return - var p = e.parse(t.getSourceCode, t.getFilePath, templateType = t.getType) - if p.hasError: - e.errors = @[p.getError] - return - if p.hasJit: - t.enableJit - e.writeAst(t, p.getStatements, e.getIndent) - freem(p) - else: - var c = newCompiler(e, p.getStatements, t, e.shouldMinify, e.getIndent, t.getFilePath) - if not c.hasError(): - e.writeHtml(t, c.getHtml()) - else: - e.errors = c.getErrors() - freem(c) - - proc precompile*(e: TimEngine, callback: proc() {.gcsafe, nimcall.} = nil, debug = false) = - ## Precompile `views` and `layouts` from `.timl` to static HTML or packed AST via MessagePack. - ## To be used in the main state of your application. - if e.templatesExists: - when not defined release: - when requires "watchout": - # Will use `watchout` to watch for changes in `/templates` dir - proc watchoutCallback(file: watchout.File) {.closure.} = - let initTime = cpuTime() - display "\n✨ Watchout resolve changes" - display file.getName() - var timView = getTemplateByPath(e, file.getPath()) - var fModified = false # to force compilation - if timView.isPartial: - for depView in timView.getDependentViews(): - let dep = getTemplateByPath(e, depView) - if dep.isModified: - fModified = true - e.compileCode(dep) - # e.compileCode(timView, fModified) - else: - e.compileCode(timView) - if e.errors.len != 0: - for err in e.errors: - display err - setLen(e.errors, 0) - else: - display("Done in: $1" % [$(cpuTime() - initTime)]) - if callback != nil: - callback() # Run a custom callback, if available - - var watchFiles: seq[string] - display("✓ Tim Templates", indent = 2) - when compileOption("threads"): - for id, view in e.getViews: - e.compileCode(view) - watchFiles.add view.getFilePath() - display(view.getName(), indent = 6) - - for id, partial in e.getPartials: - # Watch for changes in `partials` directory. - watchFiles.add partial.getFilePath() - - for id, layout in e.getLayouts: - e.compileCode(layout) - watchFiles.add layout.getFilePath() - display(layout.getName(), indent = 6) - - if e.errors.len != 0: - for err in e.errors: - display err - setLen(e.errors, 0) - # Start a new Thread with Watchout watching for live changes - startThread(watchoutCallback, watchFiles, 550) - else: - display("✓ Tim Templates", indent = 2) - for id, view in e.getViews: - e.compileCode(view) - display(view.getName(), indent = 6) - - for id, layout in e.getLayouts: - e.compileCode(layout) - display(layout.getName(), indent = 6) - if e.errors.len != 0: - for err in e.errors: - display err - setLen(e.errors, 0) - - proc tim2html*(code: string, minify = false, indent = 2, data = %*{}): string = - ## Parse snippets of timl `code` to HTML. - ## Note: calling this proc won't generate/cache AST. - var p = parser.parse(code) - if not p.hasError: - result = newCompiler(p.getStatements, minify, indent, data).getHtml - freem(p) - else: raise newException(TimParsingError, p.getError) \ No newline at end of file diff --git a/src/timpkg/engine/parser.nim b/src/timpkg/engine/parser.nim deleted file mode 100644 index cb6f449..0000000 --- a/src/timpkg/engine/parser.nim +++ /dev/null @@ -1,921 +0,0 @@ -# A blazing fast, cross-platform, multi-language -# template engine and markup language written in Nim. -# -# Made by Humans from OpenPeeps -# (c) George Lemon | LGPLv3 License -# https://github.com/openpeeps/tim - -import std/[tables, json] -# from pkg/nyml import yaml, toJsonStr -import pkg/jsony - -import ./tokens, ./ast -# from resolver import resolve, hasError, getError, -# getErrorLine, getErrorColumn, getFullCode - -from meta import Template, TemplateType, getType -from std/strutils import `%`, isDigit, join, endsWith, Newlines, - split, parseInt, parseBool, parseFloat - -type - Parser* = ref object - lvl: int - lexer: Lexer - filePath: string - includes: seq[string] - prev, curr, next: TokenTuple - parentNode: seq[Node] - enableJit: bool - hasView: bool - error: string - ast: Tree - templateType: TemplateType - ids: TableRef[string, int] - useSemantics: bool - useARIAroles: bool - ## https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles - - PrefixFunction = proc(p: var Parser): Node - # InfixFunction = proc(p: var Parser, left: Node): Node - # ErrorMessages* = enum - # invalidIndentation = "Invalid indentation" - # invalidContext = "Invalid $ in this context" - -const - invalidContext = "Invalid $1 in this context" - InvalidIndentation = "Invalid indentation" - DuplicateClassName = "Duplicate class entry \"$1\"" - InvalidAttributeId = "Invalid ID attribute" - DuplicateAttrId = "Duplicate ID entry \"$1\"" - InvalidAttributeValue = "Missing value for \"$1\" attribute" - InvalidClassAttribute = "Invalid class name" - DuplicateAttributeKey = "Duplicate attribute name \"$1\"" - InvalidTextNodeAssignment = "Expect text assignment for \"$1\" node" - UndeclaredVariable = "Undeclared variable \"$1\"" - InvalidIterationMissingVar = "Invalid iteration missing variable identifier" - InvalidIteration = "Invalid iteration" - InvalidConditionalStmt = "Invalid conditional statement" - InvalidInlineNest = "Invalid inline nest missing `>`" - InvalidNestDeclaration = "Invalid nest declaration" - InvalidValueAssignment = "Expected value after `:` assignment operator" - InvalidHTMLElementName = "Invalid HTMLElement name \"$1\"" - InvalidMixinDefinition = "Invalid mixin definition \"$1\"" - InvalidStringConcat = "Invalid string concatenation" - InvalidVarDeclaration = "Invalid variable declaration" - InvalidArrayIndex = "Invalid array access missing index" - InvalidAccessorDeclaration = "Invalid accessor declaration" - InvalidScopeVarContext = "Invalid usage of $this in this context" - InvalidGlobalVarContext = "Invalid usage of $app in this context" - NestableStmtIndentation = "Nestable statement requires indentation" - TypeMismatch = "Type mismatch: x is type of $1 but y: $2" - InvalidIDNotUnique = "The ID \"$1\" is also used for another element at line $2" - InvalidJavaScript = "Invalid JavaScript snippet" - InvalidImportView = "Trying to load a view inside a $1" - InvalidStringInterpolation = "Invalid string interpolation" - DuplicateViewLoader = "Cannot render more than one `@view`. Use `@include` instead" - -const - tkVars = {tkVariable, tkSafevariable} - tkCallables = {tkCall} - tkAssignables = {tkString, tkInteger, tkBool} + tkVars - tkComparables = tkAssignables + tkCallables - tkOperators = {tkEq, tkNe, tkLt, tkLte, tkGt, tkGte} - tkConditionals = {tkIf, tkElif, tkElse, tkIn, tkOr, tkAnd} - tkLoops = {tkFor, tkIn} - tkCalc = {tkPlus, tkMinus, tkDivide, tkMulti} - tkCallSet = {tkInclude, tkMixin} - tkNone = (tkNone, "", 0,0,0,0) - tkSpecial = {tkDot, tkColon, tkLC, tkRC, tkLP, tkRP, tkId, - tkAssign, tkComma, tkNot, tkAmp} + tkCalc + tkOperators + tkLoops - svgscTags = { - tkSvg_path, tkSvg_circle, tkSvg_polyline, tkSvg_animate, - tkSvg_animatetransform, tkSvg_animatemotion, - tkSvg_fe_blend, tkSvg_fe_colormatrix, tkSvg_fe_composite, - tkSvg_fe_convolvematrix, tkSvg_fe_displacementmap - } - scTags = { - tkArea, tkBase, tkBr, tkCol, tkEmbed, - tkHr, tkImg, tkInput, tkLink, tkMeta, - tkParam, tkSource, tkTrack, tkWbr} + svgscTags - - tkHtml = { - tkA, tkAbbr, tkAcronym, tkAddress, tkApplet, tkArea, tkArticle, tkAside, - tkAudio, tkBold, tkBase, tkBasefont, tkBdi, tkBdo, tkBig, tkBlockquote, - tkBody, tkBr, tkButton, tkCanvas, tkCaption, tkCenter, tkCite, tkCode, - tkCol, tkColgroup, tkData, tkData, tkDatalist, tkDd, tkDel, tkDetails, - tkDfn, tkDialog, tkDir, tkDoctype, tkDl, tkDt, tkEm, tkEmbed, tkFieldset, - tkFigcaption, tkFigure, tkFont, tkFooter, tkH1, tkH2, tkH3, tkH4, tkH5, tkH6, - tkHead, tkHeader, tkHr, tkHtml, tkItalic, tkIframe, tkImg, tkInput, tkIns, - tkKbd, tkLabel, tkLegend, tkLi, tkLink, tkMain, tkMap, tkMark, tkMeter, - tkNav, tkNoframes, tkNoscript, tkObject, tkOl, tkOptgroup, tkOption, tkOutput, - tkParagraph, tkParam, tkPre, tkProgress, tkQuotation, tkRp, tkRt, tkRuby, tkStrike, - tkSamp, tkSection, tkSelect, tkSmall, tkSource, tkSpan, tkStrike_long, tkStrong, - tkStyle, tkSub, tkSummary, tkSup, tkTable, tkTbody, tkTd, tkTemplate, - tkTextarea, tkTfoot, tkTh, tkThead, tkTime, tkTitle, tkTr, tkTrack, tkTt, tkUnderline, - tkUl, tkVar, tkVideo, tkWbr - } - -template setError(p: var Parser, msg: string, breakStmt: bool) = - ## Set parser error - p.error = "Error ($2:$3): $1\n$4" % [msg, $p.curr.line, $p.curr.pos, p.filePath] - break - -template setError(p: var Parser, msg: string) = - ## Set parser error - p.error = "Error ($2:$3): $1" % [msg, $p.curr.line, $p.curr.pos] - -proc setError(p: var Parser, msg: string, line, col: int, breakStmt = false) = - ## Set a parser error using a specific line/pos number - p.error = "Error ($2:$3): $1" % [msg, $line, $col] - -proc hasError*(p: var Parser): bool = - ## Determine if current parser instance has any errors - result = p.error.len != 0 or p.lexer.hasError() - -proc getError*(p: var Parser): string = - ## Retrieve current parser instance errors, - ## including lexer-side unrecognized token errors - if p.lexer.hasError(): - result = p.lexer.getError() - elif p.error.len != 0: - result = p.error - -proc isHTMLElement(token: TokenKind): bool = - result = token notin tkComparables + tkOperators + - tkConditionals + tkCalc + tkCallSet + - tkLoops + {tkEOF} - -proc parseTemplate*(code, path: string, templateType: TemplateType): Parser - -proc getAstTemplate*(p: Parser, asNodes = true): Tree = - result = p.ast - -proc getAstTemplateStr*(p: Parser, prettyString, prettyPlain = false): string = - result = toJson(p.ast) - -template jit(p: var Parser) = - ## Enable jit flag When current document contains - ## either conditionals, or variable assignments - if p.enableJit == false: - p.enableJit = true - -proc hasJIT*(p: var Parser): bool {.inline.} = - ## Determine if current timl template - ## requires a JIT compilation - result = p.enableJit == true - -proc walk(p: var Parser, offset = 1) = - var i = 0 - try: - while offset != i: - p.prev = p.curr - p.curr = p.next - p.next = p.lexer.getToken() - inc i - except IndexDefect: discard # todo toktok should take care of it - -proc `isnot`(tk: TokenTuple, kind: TokenKind): bool {.inline.} = - tk.kind != kind - -proc `is`(tk: TokenTuple, kind: TokenKind): bool {.inline.} = - tk.kind == kind - -proc `in`(tk: TokenTuple, kind: set[TokenKind]): bool {.inline.} = - tk.kind in kind - -proc `notin`(tk: TokenTuple, kind: set[TokenKind]): bool {.inline.} = - tk.kind notin kind - -proc getOperator(tk: TokenKind): InfixOp = - result = case tk: - of tkEq: EQ - of tkNe: NEQ - of tkLt: LT - of tkLte: LTE - of tkGt: GT - of tkGte: GTE - of tkAnd: AND - of tkAmp: AMP - else: None - -# prefix / infix handlers -proc parseExpressionStmt(p: var Parser): Node -proc parseRoot(p: var Parser): Node -proc parseExpression(p: var Parser, exclude: set[NodeType] = {}): Node -proc parseInfix(p: var Parser, strict = false): Node -proc parseIfStmt(p: var Parser): Node -proc parseForStmt(p: var Parser): Node -proc parseCall(p: var Parser): Node -proc getPrefixFn(p: var Parser, kind: TokenKind): PrefixFunction -# proc getInfixFn(kind: TokenKind): InfixFunction - -proc isGlobalVar(tk: TokenTuple): bool = tk.value == "app" -proc isScopeVar(tk: TokenTuple): bool = tk.value == "this" - -proc parseInteger(p: var Parser): Node = - result = ast.newInt(parseInt(p.curr.value), p.curr) - walk p - -proc parseBoolean(p: var Parser): Node = - result = ast.newBool(parseBool(p.curr.value)) - walk p - -proc parseFloat(p: var Parser): Node = - result = ast.newFloat(parseFloat(p.curr.value)) - walk p - -template handleConcat() = - while p.curr is tkAmp: - if p.next notin {tkString, tkVariable, tkSafevariable}: - p.setError(InvalidStringConcat) - return nil - walk p - let infixRight: Node = p.parseExpression() - if result == nil: - result = ast.newInfix(leftNode, infixRight, getOperator(tkAmp)) - else: - result = ast.newInfix(result, infixRight, getOperator(tkAmp)) - -proc parseString(p: var Parser): Node = - # Parse a new `string` node - if p.hasError(): return - var concated: bool - let this = p.curr - if p.next is tkAmp: - concated = true - walk p - var leftNode = ast.newString(this) - handleConcat() - if result == nil: - result = leftNode - if not concated: - result = ast.newString(this) - walk p - -proc parseVariable(p: var Parser): Node = - # Parse variables. Includes support for multi-accessor - # fields using dot notation for objects, while for array access - # by referring to the index number of the item using square brackets - var - leftNode: Node - varVisibility: VarVisibility - this = p.curr - accessors: seq[Node] - if p.curr.isGlobalVar(): - walk p - varVisibility = VarVisibility.GlobalVar - elif p.curr.isScopeVar(): - varVisibility = VarVisibility.ScopeVar - walk p - else: - varVisibility = VarVisibility.InternalVar - if p.curr in {tkDot, tkLB}: - walk p - if p.curr is tkIdentifier or (p.curr notin tkSpecial and p.curr.kind != tkEOF): - this = p.curr - walk p - while true: - if p.curr is tkEOF: - break - if p.curr is tkDot: - walk p # . - if p.curr is tkIdentifier or p.curr notin tkSpecial: - accessors.add newString(p.curr) - walk p # . - if p.curr.wsno != 0: break - else: p.setError(InvalidVarDeclaration, true) - elif p.curr.kind == tkLB: - if p.next.kind != tkInteger: - p.setError(InvalidArrayIndex, true) - walk p # [ - if p.next.kind != tkRB: - p.setError(InvalidAccessorDeclaration, true) - accessors.add(p.parseInteger()) - walk p # ] - if p.curr.wsno != 0: break - else: break - else: - case varVisibility: - of VarVisibility.GlobalVar: - p.setError(InvalidGlobalVarContext) - of VarVisibility.ScopeVar: - p.setError(InvalidScopeVarContext) - else: discard - - if p.hasError: - return # TODO handle error to avoid attempt to read from nil - - leftNode = newVariable( - this, - dataStorage = (varVisibility in {GlobalVar, ScopeVar}), - varVisibility = varVisibility - ) - leftNode.accessors = accessors - if p.curr.kind == tkAmp: # support infix concatenation X & Y - handleConcat() - if result == nil: - result = leftNode - jit p - -proc parseSafeVariable(p: var Parser): Node = - result = newVariable(p.curr, isSafeVar = true) - walk p - jit p - -template inHtmlAttributeNames(): untyped = - ( - p.curr.kind in { - tkString, tkVariable, tkSafevariable, - tkIdentifier, tkIf, tkFor, tkElif, tkElse, - tkOr, tkIn} + tkHtml and p.next.kind == tkAssign - ) - -proc getHtmlAttributes(p: var Parser): HtmlAttributes = - # Parse all attributes and return it as a - # `TableRef[string, seq[string]]` - while true: - if p.curr is tkEOF: break - if p.curr.kind == tkDot: - # Add `class=""` attribute - let attrKey = "class" - if p.next.kind notin tkSpecial: - if result.hasKey(attrKey): - # if p.next.value notin result[attrKey]: - # p.setError DuplicateClassName % [p.next.value], true - result[attrKey].add(newString(p.next)) - else: - result[attrKey] = @[newString(p.next)] - walk p, 2 - while p.curr.kind == tkLC and p.next.kind == tkVariable: - # parse string interpolation - walk p - result[attrKey][^1].sConcat.add(p.parseVariable()) - if p.curr.kind == tkRC: - walk p - else: p.setError(InvalidStringInterpolation, true) - else: - p.setError(InvalidClassAttribute) - walk p - break - elif p.curr.kind == tkId: - # Set `id=""` HTML attribute - let attrKey = "id" - if not result.hasKey(attrKey): - if p.next.kind notin tkSpecial: - walk p - if p.curr.kind in {tkVariable, tkSafevariable}: - result[attrKey] = @[p.parseVariable()] - else: - if p.ids.hasKey(p.curr.value): - p.setError InvalidIDNotUnique % [p.curr.value, $(p.ids[p.curr.value])], true - let attrValue = newString(p.curr) - result[attrKey] = @[attrValue] - p.ids[p.curr.value] = attrValue.meta.line - walk p - else: p.setError InvalidAttributeId, true - else: p.setError DuplicateAttrId % [p.next.value], true - elif inHtmlAttributeNames: - let attrName = p.curr.value - walk p - if p.next.kind notin {tkString, tkVariable, tkSafevariable}: - p.setError InvalidAttributeValue % [attrName], true - if not result.hasKey(attrName): - walk p - if p.curr.kind == tkString: - if attrName == "id": - if p.ids.hasKey(p.curr.value): - p.setError InvalidIDNotUnique % [p.curr.value, $(p.ids[p.curr.value])], true - let attrValue = newString(p.curr) - result[attrName] = @[attrValue] - if attrName == "id": - p.ids[p.curr.value] = attrValue.meta.line - walk p - else: - result[attrName] = @[p.parseVariable()] - else: - p.setError DuplicateAttributeKey % [attrName], true - # if p.curr.line > p.prev.line or p.curr.kind == tkGt: - # break - if p.curr.kind == tkGt: - break - elif p.curr.kind == tkLP: - # parse short hand conditional statement - let this = p.curr - let infixNode = p.parseInfix() - if p.curr.kind == tkSif: - walk p # ? - var ifBody = p.getHtmlAttributes() - if p.curr.kind != tkRP: - p.setError(InvalidConditionalStmt, true) - walk p # ) - let astNode = newShortIfExpression((infixNode, ifBody), this) - let condKey = "%_$1$2$3$4" % [$astNode.meta.line, $astNode.meta.pos, - $astNode.meta.col, $astNode.meta.wsno] - result[condKey] = @[astNode] - jit p - else: - p.setError(InvalidConditionalStmt, true) - elif p.curr.kind notin tkSpecial and p.prev.line == p.curr.line: - let attrName = p.curr.value - if not result.hasKey(attrName): - result[attrName] = @[] - walk p - else: - p.setError DuplicateAttributeKey % [attrName], true - else: break - -proc newHtmlNode(p: var Parser): Node = - var isSelfClosingTag = p.curr.kind in scTags - result = ast.newHtmlElement(p.curr) - result.selfCloser = isSelfClosingTag - walk p - if result.meta.pos != 0: - result.meta.pos = p.lvl * 4 # set real indentation size - while true: - if p.hasError(): return nil - if p.curr.kind == tkColon: - walk p - if p.curr.kind == tkString: - result.nodes.add p.parseString() - elif p.curr.kind in {tkVariable, tkSafevariable}: - result.nodes.add p.parseVariable() - elif p.curr.kind == tkInteger: - result.nodes.add p.parseInteger() - elif p.curr.kind == tkCall: - result.nodes.add p.parseCall() - else: - p.setError InvalidValueAssignment, p.prev.line, p.prev.col, true - # elif p.curr.kind in {tkDot, tkId, tkIdentifier} + tkHtml: - elif p.curr.kind in {tkDot, tkId} or inHtmlAttributeNames: - # if p.curr.line > result.meta.line: - # break # prevent bad loop - # if p.curr.kind == tkColon: break - result.attrs = p.getHtmlAttributes() - if p.hasError(): - break - else: break - -proc parseHtmlElement(p: var Parser): Node = - result = p.newHtmlNode() - if result == nil: - return - if p.parentNode.len == 0: - p.parentNode.add(result) - else: - if result.meta.line > p.parentNode[^1].meta.line: - p.parentNode.add(result) - var node: Node - while p.curr.kind == tkGt: - walk p - if not p.curr.kind.isHTMLElement(): - p.setError(InvalidNestDeclaration) - inc p.lvl - node = p.parseHtmlElement() - if p.curr.kind != tkEOF and p.curr.pos != 0: - if p.curr.line > node.meta.line: - let currentParent = p.parentNode[^1] - while p.curr.pos > currentParent.meta.col: - if p.curr.kind == tkEOF: break - var subNode = p.parseExpression() - if subNode != nil: - node.nodes.add(subNode) - elif p.hasError(): break - if p.curr.pos < currentParent.meta.pos: - dec p.lvl, currentParent.meta.col div p.curr.pos - delete(p.parentNode, p.parentNode.high) - break - if node != nil: - result.nodes.add(node) - elif p.hasError(): break - if p.lvl != 0: - dec p.lvl - return result - let currentParent = p.parentNode[^1] - if p.curr.pos > currentParent.meta.col: - inc p.lvl - while p.curr.pos > currentParent.meta.col: - if p.curr.kind == tkEOF: break - var subNode = p.parseExpression() - if subNode != nil: - result.nodes.add(subNode) - elif p.hasError(): break - if p.curr.kind == tkEOF or p.curr.pos == 0: break # prevent division by zero - if p.curr.pos < currentParent.meta.col: - # dec lvl, currentParent.meta.col div p.curr.pos - dec p.lvl - delete(p.parentNode, p.parentNode.high) - break - elif p.curr.pos == currentParent.meta.col: - dec p.lvl - if p.curr.pos == 0: p.lvl = 0 # reset level - -proc parseAssignment(p: var Parser): Node = - discard - -# import re # lazy house -proc parseSnippet(p: var Parser): Node = - if p.curr.kind == tkJs: - result = newSnippet(p.curr) - result.jsCode = p.curr.value # re.replace(p.curr.value, re"\/\*(.*?)\*\/|\s\B") - elif p.curr.kind == tkSass: - result = newSnippet(p.curr) - result.sassCode = p.curr.value - elif p.curr.kind in {tkJson, tkYaml}: - let code = p.curr.value.split(Newlines, maxsplit = 1) - var ident = code[0] - p.curr.value = code[1] - if p.curr.kind == tkJson: - result = newSnippet(p.curr, ident) - result.jsonCode = p.curr.value - else: - p.curr.kind = tkJson - result = newSnippet(p.curr, ident) - # result.jsonCode = yaml(p.curr.value).toJsonStr - walk p - -proc parseElseBranch(p: var Parser, elseBody: var seq[Node], ifThis: TokenTuple) = - if p.curr.pos == ifThis.pos: - var this = p.curr - walk p - if p.curr.kind == tkColon: walk p - if this.pos >= p.curr.pos: - p.setError(NestableStmtIndentation) - return - while p.curr.pos > this.pos and p.curr.kind != tkEOF: - let bodyNode = p.parseExpression(exclude = {ntInt, ntString, ntBool}) - elseBody.add bodyNode - -proc parseInfix(p: var Parser, strict = false): Node = - walk p # `if` or `(` for short hand conditions - if p.curr.kind notin tkComparables: - p.setError(InvalidConditionalStmt) - return - let - tkLeft = p.curr - infixLeftFn = p.getPrefixFn(tkLeft.kind) - var infixLeft: Node - if infixLeftFn != nil: - infixLeft = infixLeftFn(p) - if p.hasError(): return - else: - p.setError(InvalidConditionalStmt) - return - var infixNode = ast.newInfix(infixLeft) - if p.curr.kind == tkAnd: - infixNode.infixOp = getOperator(tkAnd) - while p.curr.kind == tkAnd: - infixNode.infixRight = p.parseInfix() - elif p.curr.kind in tkOperators: - let op = p.curr - walk p - if p.curr.kind notin tkComparables: - p.setError(InvalidConditionalStmt) - return - var - infixRight: Node - infixRightFn = p.getPrefixFn(p.curr.kind) - if infixRightFn != nil: - infixRight = infixRightFn(p) - if p.hasError(): return - infixNode.infixOp = op.kind.getOperator() - infixNode.infixRight = infixRight - else: - p.setError(InvalidConditionalStmt) - return - else: - infixNode = infixLeft - result = infixNode - # if strict: - # let lit = {ntInt, ntString, ntBool} - # if infixLeft.nodeType in lit and infixRight.nodeType in lit and (infixLeft.nodeType != infixRight.nodeType): - # p.setError(TypeMismatch % [infixLeft.nodeName, infixRight.nodeName]) - # result = nil - -proc parseCondBranch(p: var Parser, this: TokenTuple): IfBranch = - var infixNode = p.parseInfix() - if p.hasError(): - return - if p.curr.pos == this.pos: - p.setError(InvalidIndentation) - return - - if p.curr.kind == tkColon: - walk p - - var ifBody: seq[Node] - while p.curr.pos > this.pos and p.curr.kind != tkEOF: # parse body of `if` branch - if p.curr.kind in {tkElif, tkElse}: - p.setError(InvalidIndentation, true) - break - ifBody.add p.parseExpression() - if ifBody.len == 0: # when missing `if body` - p.setError(InvalidConditionalStmt) - return - result = (infixNode, ifBody) - -proc parseIfStmt(p: var Parser): Node = - var this = p.curr - var elseBody: seq[Node] - result = newIfExpression(ifBranch = p.parseCondBranch(this), this) - while p.curr.kind == tkElif: - let thisElif = p.curr - result.elifBranch.add p.parseCondBranch(thisElif) - if p.hasError(): break - - if p.curr.kind == tkElse: # parse body of `else` branch - p.parseElseBranch(elseBody, this) - if p.hasError(): return # catch error from `parseElseBranch` - result.elseBody = elseBody - -proc parseForStmt(p: var Parser): Node = - # Parse a new iteration statement - let this = p.curr - walk p - let singularIdent = p.parseVariable() - if p.curr.kind != tkIn: - p.setError(InvalidIteration) - return - walk p # `in` - if p.curr.kind != tkVariable: - p.setError(InvalidIteration) - return - let pluralIdent = p.parseVariable() - if p.curr.kind == tkColon: - walk p - var forBody: seq[Node] - while p.curr.pos > this.pos and p.curr.kind != tkEOF: - let subNode = p.parseExpression() - if subNode != nil: - forBody.add subNode - elif p.hasError(): break - if forBody.len != 0: - return newFor(singularIdent, pluralIdent, forBody, this) - p.setError(NestableStmtIndentation) - -proc parseCall(p: var Parser): Node = - let tk = p.curr - walk p, 2 # ident + ( - var params: seq[Node] - while p.curr.line == tk.line: - if p.curr.kind == tkRP: break - elif p.curr.kind in tkComparables: - let node = p.parseExpression() - if node != nil: - params.add(node) - else: break - elif p.curr.kind == tkComma: - walk p - else: - break - if p.curr.kind == tkRP: - walk p # ) - else: - p.setError("EOL reached before closing call statement") - return - let callIdent = tk.value - result = newCall(callIdent, params) - -proc parseMixinCall(p: var Parser): Node = - let this = p.curr - result = newMixin(p.curr) - walk p - -proc parseMixinDefinition(p: var Parser): Node = - let this = p.curr - walk p - let ident = p.curr - result = newMixinDef(p.curr) - if p.next.kind != tkLP: - p.setError(InvalidMixinDefinition % [ident.value]) - return - walk p, 2 - - while p.curr.kind != tkRP: - var paramDef: ParamTuple - if p.curr.kind == tkIdentifier: - paramDef.key = p.curr.value - walk p - if p.curr.kind == tkColon: - if p.next.kind notin {tkType_bool, tkType_int, tkType_string}: # todo handle float - p.setError(InvalidIndentation % [ident.value], true) - walk p - # todo in a fancy way, please - if p.curr.kind == tkType_bool: - paramDef.`type` = ntBool - paramDef.typeSymbol = $ntBool - elif p.curr.kind == tkType_int: - paramDef.`type` = ntInt - paramDef.typeSymbol = $ntInt - else: - paramDef.`type` = ntString - paramDef.typeSymbol = $ntString - else: - p.setError(InvalidMixinDefinition % [ident.value], true) - result.mixinParamsDef.add(paramDef) - walk p - if p.curr.kind == tkComma: - if p.next.kind != tkIdentifier: - p.setError(InvalidMixinDefinition % [ident.value], true) - walk p - walk p - while p.curr.pos > this.pos: - result.mixinBody.add p.parseExpression() - -proc parseIncludeCall(p: var Parser): Node = - result = newInclude(p.curr.value) - walk p - -proc parseRuntimeCall(p: var Parser): Node = - result = newRuntime(p.curr) - walk p - -proc parseEnd(p: var Parser): Node = - walk p - -proc parseComment(p: var Parser): Node = - # Actually, will skip comments - walk p - -proc parseAssignableNode(p: var Parser): Node = - case p.curr.kind - of tkString: p.parseString() - of tkInteger: p.parseInteger() - of tkBool: p.parseBoolean() - of tkFloat: p.parseFloat() - else: nil - -proc parseVarExpr(p: var Parser): Node = - # Parse a var declaration - let tk = p.curr - let ident = p.next - case ident.kind - of tkIdentifier: - walk p, 2 - var - varType: NodeType - varValue: Node - while true: - case p.curr.kind - of tkAssign: - walk p - varValue = p.parseAssignableNode() - if unlikely(varValue == nil): - p.setError(InvalidVarDeclaration, true) - varType = varValue.nt - break # breaks after assignment - else: break - return newVar(ident, varType, varValue) - else: discard # handle element - -proc parseViewLoader(p: var Parser): Node = - if p.templateType != ttLayout: - p.setError(InvalidImportView % [$p.templateType]) - return - elif p.hasView: - p.setError(DuplicateViewLoader) - return - result = newView(p.curr) - p.hasView = true - walk p - -proc getPrefixFn(p: var Parser, kind: TokenKind): PrefixFunction = - result = case kind - of tkInteger: parseInteger - of tkBool: parseBoolean - of tkString: parseString - of tkFloat: parseFloat - of tkIf: parseIfStmt - of tkFor: parseForStmt - of tkVar: parseVarExpr - of tkInclude: parseIncludeCall - of tkJs, tkSass, tkJson, tkYaml: parseSnippet - of tkWasm: parseRuntimeCall - # of tkEnd: parseEnd - of tkMixin: - if p.next.kind == tkLP: - parseMixinCall - elif p.next.kind == tkIdentifier: - parseMixinCall - else: nil - of tkCall: - if p.next.kind == tkLP: - parseCall - else: nil - of tkVariable: parseVariable - of tkSafevariable: parseSafeVariable - of tkComment: parseComment - of tkView: parseViewLoader - else: parseHtmlElement - -proc parseExpression(p: var Parser, exclude: set[NodeType] = {}): Node = - var - this = p.curr - prefixFunction = p.getPrefixFn(this.kind) - exp: Node = p.prefixFunction() - if exp == nil: return - if exclude.len != 0: - if exp.nt in exclude: - p.setError("Unexpected token \"$1\"" % [this.value]) - result = exp - -proc parseExpressionStmt(p: var Parser): Node = - var exp = p.parseExpression() - if exp == nil or p.hasError(): - return - result = ast.newExpression exp - -proc parseRoot(p: var Parser): Node = - case p.templateType - of ttView, ttPartial: - result = p.parseExpressionStmt() - else: - let prefixFunction = - case p.curr.kind - of tkInteger, tkBool, tkString, tkFloat: - p.setError(invalidContext) - nil - of tkIf: parseIfStmt - of tkFor: parseForStmt - of tkVar: parseVarExpr - of tkInclude: parseIncludeCall - of tkJs, tkSass, tkJson, tkYaml: parseSnippet - of tkWasm: parseRuntimeCall - # of tkEnd: parseEnd - of tkMixin: - if p.next.kind == tkLP: - parseMixinCall - elif p.next.kind == tkIdentifier: - parseMixinCall - else: nil - of tkCall: - if p.next.kind == tkLP: - parseCall - else: nil - of tkVariable: parseVariable - of tkSafeVariable: parseSafeVariable - of tkView: parseViewLoader - else: - if p.curr.value in ["head", "body"]: - parseHtmlElement - else: - p.setError(invalidContext % [p.curr.value]) - nil - if likely prefixFunction != nil: - let rootNode: Node = p.prefixFunction() - if rootNode != nil: - result = ast.newExpression(rootNode) - # case p.curr.kind: - # of tkVariable: - # result = p.parseVariable() - # else: - # result = p.parseExpressionStmt() - -proc parseTemplate*(tpl: Template): Parser = - ## Parse `tpl` Template - var p = Parser(ids: newTable[string, int](), templateType: tpl.getType()) - p.lexer = newLexer(readFile(tpl.sources.src), allowMultilineStrings = true) - # p.tpl = tpl - p.curr = p.lexer.getToken - p.next = p.lexer.getToken - while not p.hasError and p.curr isnot tkEOF: - var statement: Node = p.parseRoot() - if likely(statement != nil): - p.ast.nodes.add(statement) - else: break # error? - p.lexer.close - result = p - -proc parseTemplate*(code, path: string, templateType: TemplateType): Parser = - ## Parse a new Tim document - var p: Parser = Parser(ids: newTable[string, int]()) - # if p.templateType == ttLayout: - # jit(p) # force enabling jit for layout templates - p.lexer = newLexer(code, allowMultilineStrings = true) - p.filePath = path - p.curr = p.lexer.getToken() - p.next = p.lexer.getToken() - # p.tpl = Template() - while p.hasError() == false and p.curr.kind != tkEOF: - var statement: Node = p.parseRoot() - if statement != nil: - p.ast.nodes.add(statement) - p.lexer.close() - result = p - -proc parseTemplate*(code: string): Parser = - var p = Parser(ids: newTable[string, int](), templateType: ttView) - p.lexer = newLexer(code, allowMultilineStrings = true) - p.curr = p.lexer.getToken - p.next = p.lexer.getToken - while p.hasError == false and p.curr.kind != tkEOF: - var stmtNode: Node = p.parseRoot() - if stmtNode != nil: - p.ast.nodes.add(stmtNode) - p.lexer.close() - result = p - -when isMainModule: - ## Test Tim parser - var p = parseTemplate("""div.container > div.row > div.col-12""") - assert p.hasError() == false - echo p.getAstTemplate \ No newline at end of file diff --git a/src/timpkg/engine/private/jitutils.nim b/src/timpkg/engine/private/jitutils.nim deleted file mode 100644 index b8b5a7c..0000000 --- a/src/timpkg/engine/private/jitutils.nim +++ /dev/null @@ -1,400 +0,0 @@ -# A high-performance compiled template engine inspired by the Emmet syntax. -# -# (c) 2023 George Lemon | MIT License -# Made by Humans from OpenPeep -# https://github.com/openpeep/tim - -proc getVarValue(c: var Compiler, varNode: Node): string = - result = c.data[varNode.varIdent].getStr - if varNode.dataStorage: - if varNode.isSafeVar: - result = multiReplace(result, - ("^", "&"), - ("<", "<"), - (">", ">"), - ("\"", """), - ("'", "'"), - ("`", "`") - ) - -proc getVarTypeStr(varVisibility: VarVisibility): string = - result = - if varVisibility == GlobalVar: - "globals" - else: - "scope" - -proc getJsonData(c: var Compiler, key: string): JsonNode = - if c.data.hasKey(key): - result = c.data[key] - else: - var checkNext = true - if c.data.hasKey("globals"): - if c.data["globals"].hasKey(key): - result = c.data["globals"][key] - checkNext = false - if checkNext: - if c.data.hasKey("scope"): - if c.data["scope"].hasKey(key): - result = c.data["scope"][key] - checkNext = false - if checkNext: - result = newJNull() - -proc getJsonValue(c: var Compiler, node: Node, jsonNodes: JsonNode): JsonNode = - if node.accessors.len == 0: - return jsonNodes - var - lvl = 0 - levels = node.accessors.len - propNode = node.accessors[lvl] - proc getJValue(c: var Compiler, jN: JsonNode): JsonNode = - if propNode.nodeType == NTInt: - if jN.kind == JArray: - let jNSize = jN.len - if propNode.iVal > (jNSize - 1): - c.logs.add(ArrayIndexOutBounds % [$propNode.iVal, node.varIdent, $(jNSize)]) - else: - result = jN[propNode.iVal] - inc lvl - if levels > lvl: - propNode = node.accessors[lvl] - result = c.getJValue(result) - else: - if propNode.nodeType == NTString: - c.logs.add(InvalidArrayAccess % [node.varIdent, propNode.sVal]) - else: c.logs.add(UndefinedArray) - elif propNode.nodeType == NTString: - if jN.kind == JObject: - if jn.hasKey(propNode.sVal): - result = jN[propNode.sVal] - inc lvl - if levels > lvl: - propNode = node.accessors[lvl] - result = c.getJValue(result) - else: c.logs.add(UndefinedProperty % [propNode.sVal]) - else: c.logs.add(UndefinedProperty % [propNode.sVal]) - result = c.getJValue(jsonNodes) - -proc getValue(c: var Compiler, node: Node): JsonNode = - # if node.dataStorage == false and node.accessors.len == 0 and c.memtable.hasKey(node.varSymbol) == false: - # discard # TODO - # result = c.data["globals"][node.varIdent] - if node.dataStorage == false and c.memtable.hasKey(node.varSymbol): - result = c.memtable[node.varSymbol] - case result.kind: - of JObject: - result = c.getJsonValue(node, result) - else: discard - elif node.visibility == GlobalVar and c.data.hasKey("globals"): - if node.accessors.len == 0: - if c.data["globals"].hasKey(node.varIdent): - result = c.data["globals"][node.varIdent] - else: c.logs.add(UndefinedVariable % [node.varIdent, "globals"]) - else: - if c.data["globals"].hasKey(node.varIdent): - let jsonNode = c.data["globals"][node.varIdent] - result = c.getJsonValue(node, jsonNode) - else: c.logs.add(UndefinedVariable % [node.varIdent, "globals"]) - elif node.visibility == ScopeVar and c.data.hasKey("scope"): - if c.data["scope"].hasKey(node.varIdent): - let jsonNode = c.data["scope"][node.varIdent] - result = c.getJsonValue(node, jsonNode) - else: c.logs.add(UndefinedVariable % [node.varIdent, "scope"]) - else: - result = newJNull() - -proc getStringValue(c: var Compiler, node: Node): string = - let jsonValue = c.getValue(node) - if jsonValue == nil: return - case jsonValue.kind: - of JString: add result, jsonValue.getStr - of JInt: add result, $(jsonValue.getInt) - of JFloat: add result, $(jsonValue.getFloat) - of JBool: add result, $(jsonValue.getBool) - of JObject, JArray, JNull: - c.logs.add(InvalidConversion % [$jsonValue.kind, node.varIdent]) - c.fixTail = true - -proc storeValue(c: var Compiler, symbol: string, item: JsonNode) = - c.memtable[symbol] = item - -macro `?`*(a: bool, body: untyped): untyped = - let b = body[1] - let c = body[2] - result = quote: - if `a`: `b` else: `c` - -macro isEqualBool*(a, b: bool): untyped = - result = quote: - `a` == `b` - -macro isNotEqualBool*(a, b: bool): untyped = - result = quote: - `a` != `b` - -macro isEqualInt*(a, b: int): untyped = - result = quote: - `a` == `b` - -macro isNotEqualInt*(a, b: int): untyped = - result = quote: - `a` != `b` - -macro isGreaterInt*(a, b: int): untyped = - result = quote: - `a` > `b` - -macro isGreaterEqualInt*(a, b: int): untyped = - result = quote: - `a` >= `b` - -macro isLessInt*(a, b: int): untyped = - result = quote: - `a` < `b` - -macro isLessEqualInt*(a, b: int): untyped = - result = quote: - `a` <= `b` - -macro isEqualFloat*(a, b: float64): untyped = - result = quote: - `a` == `b` - -macro isNotEqualFloat*(a, b: float64): untyped = - result = quote: - `a` != `b` - -macro isEqualString*(a, b: string): untyped = - result = quote: - `a` == `b` - -macro isNotEqualString*(a, b: string): untyped = - result = quote: - `a` != `b` - -proc handleInfixStmt(c: var Compiler, node: Node) = - if node.infixOp == AMP: - if node.infixLeft.nodeType == NTVariable: - add c.html, c.getStringValue(node.infixLeft) - elif node.infixLeft.nodeType == NTString: - c.writeStrValue(node.infixLeft) - elif node.infixLeft.nodeType == NTInfixStmt: - c.handleInfixStmt(node.infixLeft) - - if node.infixRight.nodeType == NTVariable: - add c.html, c.getStringValue(node.infixRight) - elif node.infixRight.nodeType == NTString: - c.writeStrValue(node.infixRight) - elif node.infixRight.nodeType == NTInfixStmt: - c.handleInfixStmt(node.infixRight) - -proc aEqualB(c: var Compiler, a: JsonNode, b: Node, swap: bool): bool = - if a.kind == JString and b.nodeType == NTString: - result = isEqualString(a.getStr, b.sVal) - elif a.kind == JInt and b.nodeType == NTInt: - result = isEqualInt(a.getInt, b.iVal) - elif a.kind == JBool and b.nodeType == NTBool: - result = isEqualBool(a.getBool, b.bVal) - else: - if swap: c.logs.add(InvalidComparison % [b.nodeName, $a.kind]) - else: c.logs.add(InvalidComparison % [$a.kind, b.nodeName]) - -proc aNotEqualB(c: var Compiler, a: JsonNode, b: Node, swap: bool): bool = - if a.kind == JString and b.nodeType == NTString: - result = isNotEqualString(a.getStr, b.sVal) - elif a.kind == JInt and b.nodeType == NTInt: - result = isNotEqualInt(a.getInt, b.iVal) - elif a.kind == JBool and b.nodeType == NTBool: - result = isNotEqualBool(a.getBool, b.bVal) - else: - if swap: c.logs.add(InvalidComparison % [b.nodeName, $a.kind]) - else: c.logs.add(InvalidComparison % [$a.kind, b.nodeName]) - -proc compareVarLit(c: var Compiler, leftNode, rightNode: Node, op: OperatorType, swap = false): bool = - var jd: JsonNode - var continueCompare: bool - if leftNode.dataStorage: - jd = c.getJsonData(leftNode.varIdent) - if jd.kind != JNull: - if jd.kind in {JArray, JObject}: - jd = c.getJsonValue(leftNode, jd) - continueCompare = true - else: - if c.memtable.hasKey(leftNode.varSymbol): - jd = c.getJsonValue(leftNode, c.memtable[leftNode.varSymbol]) - if jd.kind != JNull: - continueCompare = true - if continueCompare: - case op - of EQ: result = c.aEqualB(jd, rightNode, swap) - of NE: result = c.aNotEqualB(jd, rightNode, swap) - of GT: - if jd.kind == JInt: - if swap: result = isLessInt(jd.getInt, rightNode.iVal) - else: result = isGreaterInt(jd.getInt, rightNode.iVal) - of GTE: - if jd.kind == JInt: - if swap: result = isLessEqualInt(jd.getInt, rightNode.iVal) - else: result = isGreaterEqualInt(jd.getInt, rightNode.iVal) - of LT: - if jd.kind == JInt: - if swap: result = isGreaterInt(jd.getInt, rightNode.iVal) - else: result = isLessInt(jd.getInt, rightNode.iVal) - of LTE: - if jd.kind == JInt: - if swap: result = isGreaterEqualInt(jd.getInt, rightNode.iVal) - else: result = isLessEqualInt(jd.getInt, rightNode.iVal) - else: discard - -proc compVarNil(c: var Compiler, node: Node): bool = - # Evaluate NTVariable if returning value is anything but null - # Example: `if $myvar:` - var j = newJNull() - if c.memtable.hasKey(node.varSymbol): - # Check if is available in memtable - j = c.getJsonValue(node, c.memtable[node.varSymbol]) - else: - j = c.getJsonData(node.varIdent) - if j == nil: - return - if j.kind == JBool: - result = j.getBool == true - # elif j.kind == JNull: not needed - # c.logs.add(UndefinedVariable % [node.varSymbol, getVarTypeStr(node.visibility)]) - else: - result = j.len != 0 - -proc tryGetFromMemtable(c: var Compiler, node: Node): JsonNode = - if c.memtable.hasKey(node.varSymbol): - return c.getJsonValue(node, c.memtable[node.varSymbol]) - result = nil - -proc getFnParams(c: var Compiler, node: Node, paramCount: int): seq[string] = - var i = 0 - for param in node.callParams: - if param.nodeType == NTVariable: - var strParam = c.tryGetFromMemtable(param) - if strParam == nil: - strParam = c.getJsonData(param.varIdent) - add result, strParam.getStr - else: - add result, param.sVal - inc i - while paramCount > i: - add result, newString(0) - inc i - -proc compInfixNode(c: var Compiler, node: Node): bool = - if node.nodeType == NTVariable: - result = c.compVarNil(node) - elif node.nodeType == NTInfixStmt: - case node.infixOp - of EQ, NE: - if node.infixLeft.nodeType == node.infixRight.nodeType: - # compare two values sharing the same type - case node.infixLeft.nodeType: - of NTInt: - result = isEqualInt(node.infixLeft.iVal, node.infixRight.iVal) - of NTString: - result = isEqualString(node.infixLeft.sVal, node.infixRight.sVal) - of NTVariable: - var lNode = c.getJsonData(node.infixLeft.varIdent) - var rNode = c.getJsonData(node.infixRight.varIdent) - if lNode != nil and rNode != nil: - result = lNode == rNode: - elif lNode != nil and rNode == nil: - if c.memtable.hasKey(node.infixRight.varSymbol): - rNode = c.getJsonValue(node.infixRight, c.memtable[node.infixRight.varSymbol]) - result = lNode == rNode - else: discard - elif node.infixLeft.nodeType == NTVariable and node.infixRight.nodeType in {NTBool, NTString, NTInt}: - # compare `NTVariable == {NTBool, NTString, NTInt}` - result = c.compareVarLit(leftNode = node.infixLeft, op = node.infixOp, rightNode = node.infixRight) - elif node.infixLeft.nodeType in {NTBool, NTString, NTInt} and node.infixRight.nodeType == NTVariable: - # compare `{NTBool, NTString, NTInt} == NTVariable` - result = c.compareVarLit(leftNode = node.infixRight, op = node.infixOp, rightNode = node.infixLeft, true) - of GT, GTE, LT, LTE: - if node.infixLeft.nodeType == node.infixRight.nodeType: - case node.infixLeft.nodeType: - of NTInt: - case node.infixOp: - of GT: - result = isGreaterInt(node.infixLeft.iVal, node.infixRight.iVal) - of GTE: - result = isGreaterEqualInt(node.infixLeft.iVal, node.infixRight.iVal) - of LT: - result = isLessInt(node.infixLeft.iVal, node.infixRight.iVal) - of LTE: - result = isLessEqualInt(node.infixLeft.iVal, node.infixRight.iVal) - else: discard - else: discard - - elif node.infixLeft.nodeType == NTVariable and node.infixRight.nodeType == NTInt: - result = c.compareVarLit(node.infixLeft, node.infixRight, node.infixOp) - elif node.infixleft.nodeType == NTInt and node.infixRight.nodeType == NTVariable: - result = c.compareVarLit(node.infixRight, node.infixLeft, node.infixOp, true) - else: - c.logs.add(InvalidComparison % [ - node.infixLeft.nodeName, node.infixRight.nodeName - ]) - of AND: - if node.infixLeft.nodeType == NTVariable and node.infixRight.nodeType == NTInfixStmt: - result = c.compVarNil(node.infixLeft) - if result: - result = c.compInfixNode(node.infixRight) - elif node.infixLeft.nodeType == NTInfixStmt and node.infixRight.nodeType == NTVariable: - result = c.compVarNil(node.infixRight) - if result: - result = c.compInfixNode(node.infixLeft) - elif node.infixLeft.nodeType == NTVariable and node.infixRight.nodeType == NTCall: - result = c.compVarNil(node.infixLeft) - if result: - let fn = c.engine.imports[node.infixRight.callIdent] - result = fn.boolFn(getFnParams(c, node.infixRight, fn.paramCount)) - else: discard - elif node.nodeType == NTCall: - if c.engine.imports.hasKey(node.callIdent): - let fn = c.engine.imports[node.callIdent] - result = fn.boolFn(getFnParams(c, node, fn.paramCount)) - -proc handleConditionStmt(c: var Compiler, node: Node) = - if c.compInfixNode(node.ifCond): - c.writeNewLine(node.ifBody) - elif node.elifBranch.len != 0: - var skipElse: bool - for elifNode in node.elifBranch: - if c.compInfixNode(elifNode.cond): - c.writeNewLine(elifNode.body) - skipElse = true - break - if not skipElse and node.elseBody.len != 0: - c.writeNewLine(node.elseBody) - else: - if node.elseBody.len != 0: - c.writeNewLine(node.elseBody) - -proc handleForStmt(c: var Compiler, forNode: Node) = - let jsonItems = c.getValue(forNode.forItems) - if jsonItems != nil: - case jsonItems.kind: - of JArray: - for item in jsonItems: - c.storeValue(forNode.forItem.varSymbol, item) - c.writeNewLine(forNode.forBody) - c.memtable.del(forNode.forItem.varSymbol) - of JObject: - for k, v in pairs(jsonItems): - var kvObject = newJObject() - kvObject[k] = v - c.storeValue(forNode.forItem.varSymbol, kvObject) - c.writeNewLine(forNode.forBody) - c.memtable.del(forNode.forItem.varSymbol) - else: discard - -proc callFunction(c: var Compiler, node: Node) = - ## Execute a function that returns a string - if c.engine.imports.hasKey(node.callIdent): - let fn = c.engine.imports[node.callIdent] - add c.html, fn.strFn(getFnParams(c, node, fn.paramCount)) - c.fixTail = true \ No newline at end of file diff --git a/src/timpkg/engine/private/stdcalls.nim b/src/timpkg/engine/private/stdcalls.nim deleted file mode 100644 index 06d9506..0000000 --- a/src/timpkg/engine/private/stdcalls.nim +++ /dev/null @@ -1,18 +0,0 @@ -template typeSafety(params: varargs[tuple[pKind, pExpectKind: JsonNodeKind]]) = - for p in params: - if p.pKind != p.pExpectKind: - c.logs.add("Type mismatch, got $1 but expected $2" % [$p.pKind, $p.pExpectKind]) - return - -# proc callStdStartsWith(c: var Compiler, params: seq[Node]): bool = -# var strParam, prefixParam: JsonNode -# if params[0].nodeType == NTVariable: -# strParam = c.tryGetFromMemtable(params[0]) -# if strParam == nil: -# strParam = c.getJsonData(params[0].varIdent) -# if params[1].nodeType == NTVariable: -# prefixParam = c.tryGetFromMemtable(params[1]) -# if prefixParam == nil: -# prefixParam = c.getJsonData(params[1].varIdent) -# typeSafety((strParam.kind, JString), (prefixParam.kind, JString)) -# result = strutils.startsWith(strParam.getStr, prefixParam.getStr) diff --git a/src/timpkg/engine/private/transpiler.nim b/src/timpkg/engine/private/transpiler.nim deleted file mode 100644 index 5546e72..0000000 --- a/src/timpkg/engine/private/transpiler.nim +++ /dev/null @@ -1,433 +0,0 @@ -# A high-performance compiled template -# engine inspired by the Emmet syntax. -# -# (c) 2023 Tim Engine | MIT License -# Made by Humans from OpenPeep -# https://github.com/openpeep/tim - -import std/[tables, ropes, macros] -import ../ast, ../tokens -import pkg/sass - -from std/strutils import `%`, indent, multiReplace, join, escape -from ../meta import TimlTemplate, setPlaceHolderId - -type - Language* = enum - Nim = "nim" - JavaScript = "js" - Python = "python" - Php = "php" - - Compiler* = object - ## Compiles current AST program to HTML or SCF (Source Code Filters) - program: Program - ## All Nodes statements under a `Program` object instance - language: Language - timView: TimlTemplate - minify: bool - ## Whether to minify the final HTML output (disabled by default) - html, js, sass: Rope - ## A rope containg the final HTML output - baseIndent: int - ## Document base indentation - logs: seq[string] - ## Store errors at runtime without breaking the process - hasViewCode, hasJS, hasSass: bool - viewCode: string - ## When compiler is initialized for layout, - ## this field will contain the view code (HTML) - fixTail: bool - prev, next: NodeType - firstParentNode: MetaNode - -const - NewLine = "\n" - InvalidAccessorKey = "Invalid property accessor \"$1\" for $2 ($3)" - InvalidConversion = "Failed to convert $1 \"$2\" to string" - InvalidComparison = "Can't compare $1 and $2 values" - InvalidObjectAccess = "Invalid object access" - UndefinedPropertyAccessor = "Undefined property accessor \"$1\" in data storage" - UndefinedArray = "Undefined array" - InvalidArrayAccess = "Array indices must be positive integers. Got $1[\"$2\"]" - ArrayIndexOutBounds = "Index out of bounds [$1]. \"$2\" size is [$3]" - UndefinedProperty = "Undefined property \"$1\"" - UndefinedVariable = "Undefined property \"$1\" in \"$2\"" - -var langs = { - "nim": { - "if": "if $1:", - "elif": "elif $1:", - "else": "else:", - "fn": "proc render$1View[G, S](app: G, this: S) =", - "for": "for $1 in $2" - }, - "js": { - "if": "if($1) {", - "elif": "} else if($1) {", - "else": "} else {", - "fn": "function render$1View(app = {}, this = {}) {$2}", - "for": "" - } -}.toTable - -proc writeNewLine(c: var Compiler, nodes: seq[Node]) # defer -proc getNewLine(c: var Compiler, nodes: seq[Node]): string # defer - -proc getIndent(c: var Compiler, nodeIndent: int): int = - if c.baseIndent == 2: - return int(nodeIndent / c.baseIndent) - result = nodeIndent - -proc getIndentLine(c: var Compiler, meta: MetaNode, skipBr = false): string = - if meta.pos != 0: - if not skipBr: - add result, NewLine - add result, indent("", c.getIndent(meta.pos)) - else: - if not skipBr: - add result, NewLIne - -proc indentLine(c: var Compiler, meta: MetaNode, skipBr = false) = - add c.html, c.getIndentLine(meta, skipBr) - -proc getIDAttribute(c: var Compiler, node: Node): string = - ## Write an ID HTML attribute to current HTML Element - add result, indent("id=", 1) & "\"" - let idAttrNode = node.attrs["id"][0] - if idAttrNode.nodeType == NTString: - add result, idAttrNode.sVal - # else: c.writeValue(idAttrNode) - add result, "\"" - # add c.html, ("id=\"$1\"" % [node.attrs["id"][0]]).indent(1) - -proc getAttributes(c: var Compiler, node: Node): string = - ## write one or more HTML attributes - for k, attrNodes in node.attrs.pairs(): - if k == "id": continue # handled by `writeIDAttribute` - add result, indent("$1=" % [k], 1) & "\"" - var strAttrs: seq[string] - for attrNode in attrNodes: - if attrNode.nodeType == NTString: - strAttrs.add attrNode.sVal - elif attrNode.nodeType == NTVariable: - # TODO handle concat - discard - # c.writeValue(attrNode) - if strAttrs.len != 0: - add result, join(strAttrs, " ") - add result, "\"" - # add c.html, ("$1=\"$2\"" % [k, join(v, " ")]).indent(1) - -proc writeStrValue(c: var Compiler, node: Node) = - add c.html, node.sVal - c.fixTail = true - -proc writeIntValue(c: var Compiler, node: Node) = - add c.html, $node.iVal - c.fixTail = true - -proc getOpenTag(c: var Compiler, tag: string, node: Node, skipBr = false): string = - if not c.minify: - add result, c.getIndentLine(node.meta, skipBr = skipBr) - add result, "<" & tag - if node.attrs.hasKey("id"): - add result, c.getIDAttribute(node) - if node.attrs.len != 0: - add result, c.getAttributes(node) - if node.issctag: - add result, "/" - add result, ">" - -proc openTag(c: var Compiler, tag: string, node: Node, skipBr = false) = - ## Open tag of the current JsonNode element - add c.html, c.getOpenTag(tag, node) - -proc getCloseTag(c: var Compiler, node: Node, skipBr: bool): string = - ## Close an HTML tag - if node.issctag == false: - if not c.fixTail and not c.minify: - add result, c.getIndentLine(node.meta, skipBr) - add result, "" - -proc closeTag(c: var Compiler, node: Node, skipBr = false) = - ## Close an HTML tag - if node.issctag == false: - add c.html, c.getCloseTag(node, skipBr) - -proc newResult(c: var Compiler, meta: MetaNode) = - let pos = if meta.col == 0: 2 - else: meta.col + 2 - add c.html, NewLine - case c.language: - of Nim: - c.html &= indent("result &= \"\"\"", pos) - of Php: - c.html &= indent("$result = \"\";", pos) # define $result var - c.html &= NewLine - c.html &= indent("$result .= <<$`(ident: string) = - case c.language: - of Nim: - add result, $TK_DOT & n.sVal - of JavaScript: - discard - of Python: - discard - of Php: - add result, $TK_MINUS & $TK_GT & n.sVal - -template `>$`(i: int) = - add result, "[" & $(i) & "]" - -template `{`() = - if braces: - case c.language: - of Nim: - result = "\"\"\" & fmt(\"{" - of Php: - result = "{$" - else: discard # TODO - -template `}`() = - if braces: - case c.language: - of Nim: - add result, "}\") & \"\"\"" - of Php: - add result, "}" - else: discard # TODO - -proc getIdent(c: var Compiler, node: Node, braces = false): string = - case node.nodeType: - of NTInt: - result = $(node.iVal) - of NTBool: - result = $(node.bVal) - of NTVariable: - `{` - case node.visibility: - of GlobalVar: - case c.language: - of Nim: - add result, "app" & $TK_DOT - of JavaScript: - discard - of Python: - discard - of Php: - add result, "app" & $TK_MINUS & $TK_GT - of ScopeVar: - add result, "this" & $TK_DOT - else: discard # InternalVar - # var accessorTk: string - add result, node.varIdent - if node.accessors.len != 0: - for n in node.accessors: - if n.nodeType == NTString: - >$ n.sVal - else: - >$ n.iVal - `}` - of NTString: - result = node.sVal - else: discard - -proc newInfixOp(c: var Compiler, a, b: Node, op: OperatorType, tkCond = TK_IF): string = - result = $tkCond - result &= indent(c.getIdent(a), 1) - result &= indent($op, 1) - result &= indent(c.getIdent(a), 1) - result &= $TK_COLON - -template br() = - c.html &= NewLine - -proc handleConditionStmt(c: var Compiler, node: Node) = - br - var i = if node.meta.col == 0: 2 else: node.meta.col + 2 - var infixCond = c.newInfixOp(node.ifCond.infixLeft, node.ifCond.infixRight, node.ifCond.infixOp) - add c.html, indent(infixCond, i) - c.writeNewLine(node.ifBody) - if node.elifBranch.len != 0: - for elifNode in node.elifBranch: - c.endResult() - br - infixCond = c.newInfixOp( - elifNode.cond.infixLeft, - elifNode.cond.infixRight, - elifNode.cond.infixOp, - TK_ELIF - ) - add c.html, indent(infixCond, i) - c.prev = NTConditionStmt - c.writeNewLine(elifNode.body) - c.endResult(true) - c.endResult(true) - if node.elseBody.len != 0: - var elseTk = $TK_ELSE & $TK_COLON - c.html &= indent(elseTk, i) - c.prev = NTConditionStmt - for n in node.elseBody: - c.writeNewLine(node.elseBody) - c.endResult(true) - -proc handleForStmt(c: var Compiler, node: Node) = - c.endResult() - var forStmt = indent("\nfor $1 in $2:" % [node.forItem.varIdent, c.getIdent(node.forItems)], node.meta.col) - c.html &= forStmt - if node.forBody[0].nodeType == NTHtmlElement: - c.newResult(node.forBody[0].meta) - c.writeNewLine(node.forBody) - c.endResult() - c.newResult(c.firstParentNode) - -proc getNewLine(c: var Compiler, nodes: seq[Node]): string = - for node in nodes: - if node == nil: continue - case node.nodeType: - of NTHtmlElement: - let tag = node.htmlNodeName - add result, c.getOpenTag(tag, node) - if node.nodes.len != 0: - discard c.getNewLine(node.nodes) - add result, c.getCloseTag(node, false) - if c.fixTail: c.fixTail = false - of NTConditionStmt: - c.handleConditionStmt(node) - of NTString: - add result, c.getIdent(node) - else: discard - -proc writeNewLine(c: var Compiler, nodes: seq[Node]) = - # if nodes[0].nodeType == NTHtmlElement: - # add c.html, NewLine - # add c.html, indent("result.add(\"\"\"", nodes[0].meta.col * 2) - for node in nodes: - if node == nil: continue # TODO - case node.nodeType: - of NTHtmlElement: - let tag = node.htmlNodeName - c.openTag(tag, node) - if node.nodes.len != 0: - c.writeNewLine(node.nodes) - c.closeTag(node, false) - if c.fixTail: - c.fixTail = false - of NTConditionStmt: - c.handleConditionStmt(node) - of NTForStmt: - c.handleForStmt(node) - of NTVariable: - add c.html, c.getIdent(node, true) - c.fixTail = true - of NTView: - c.insertViewCode() - of NTString: - c.writeStrValue(node) - of NTInt: - c.writeIntValue(node) - of NTJavaScript: - c.hasJs = true - c.handleJavaScriptSnippet(node) - of NTSass: - c.hasSass = true - c.handleSassSnippet(node) - else: discard - -proc newCompiler*(program: Program, t: TimlTemplate, minify: bool, - indent: int, filePath: string, - viewCode = "", lang = Nim): Compiler = - var c = Compiler( - language: lang, - program: program, - timView: t, - minify: false, - baseIndent: 2 - ) - - case c.language - of Nim: - c.html &= "proc renderProductsView[G, S](app: G, this: S): string =" - if c.program.nodes.len == 0: - c.html &= NewLine & indent("discard", 2) - of JavaScript: - c.html &= "function renderProductsView(app = {}, this = {}) {" - of Python: - c.html &= "def renderProductsView(app: Dict, this: Dict):" - of Php: - c.html &= "function renderProductsView(object $app, object $this) {" - - var metaNode: MetaNode - if c.program.nodes.len != 0: - if c.program.nodes[0].stmtList.nodeType == NTHtmlElement: - c.newResult(metaNode) - for node in c.program.nodes: - case node.stmtList.nodeType: - of NTHtmlElement: - let tag = node.stmtList.htmlNodeName - c.firstParentNode = node.meta - c.openTag(tag, node.stmtList) - if node.stmtList.nodes.len != 0: - c.writeNewLine(node.stmtList.nodes) - c.closeTag(node.stmtList) - of NTConditionStmt: - c.endResult() - c.handleConditionStmt(node.stmtList) - of NTForStmt: - c.handleForStmt(node.stmtList) - of NTVariable: - add c.html, c.getIdent(node) - of NTView: - c.insertViewCode() - of NTJavaScript: - c.hasJs = true - c.handleJavaScriptSnippet(node) - of NTSass: - c.hasSass = true - c.handleSassSnippet(node) - else: discard - c.endResult() - - var insertSnippets = c.hasJS or c.hasSass - if insertSnippets: - c.newResult(metaNode) - if c.hasJS: - add c.html, NewLine & "" - if c.hasSass: - add c.html, NewLine & "" - if insertSnippets: - c.endResult() - - case c.language: - of Php, JavaScript: - c.html &= NewLine - c.html &= indent("return $result;", 2) - c.html &= NewLine & "}" - else: discard - result = c \ No newline at end of file diff --git a/src/timpkg/engine/resolver.nim b/src/timpkg/engine/resolver.nim deleted file mode 100644 index 2aee522..0000000 --- a/src/timpkg/engine/resolver.nim +++ /dev/null @@ -1,160 +0,0 @@ -# A high-performance compiled template engine -# inspired by the Emmet syntax. -# -# (c) 2023 George Lemon | MIT License -# Made by Humans from OpenPeeps -# https://github.com/openpeeps/tim - -import toktok -import std/[streams, tables, ropes] - -from ./meta import TimEngine, TemplateType, Template, - addDependentView, getTemplateByPath, getPathDir - -from std/sequtils import concat, deduplicate -from std/strutils import endsWith, `%`, indent -from std/os import getCurrentDir, parentDir, fileExists, normalizedPath - -import ./tokens - -## Resolve all `@include` calls inside a `view`, `layout` or `partial`. - -type - SourcePath = string - ## Partial Source Path - - SourceCode = string - ## Partial Source Code - - Importer* = object - engine: TimEngine - ## An instance of `TimEngine` - lex: Lexer - ## An instance of `TokTok` Lexer - rope: Rope - ## The entire view containing resolved partials - error: string - ## An error message to be shown - error_line, error_column: int - ## Error line and column - error_trace: string - ## A preview of a ``.timl`` code highlighting error - currentFilePath: string - ## The absoulte file path for ``.timl`` view - current, next: TokenTuple - partials: OrderedTable[int, tuple[indentSize: int, source: SourcePath]] - ## An ``OrderedTable`` with ``int`` based key representing - ## the line of the ``@import`` statement and a tuple-based value. - ## - ``indentSize`` field to preserve indentation size from the view side - ## - ``source`` field pointing to an absolute path for ``.timl`` partial. - sources: Table[SourcePath, SourceCode] - ## A ``Table`` containing the source code of all imported partials. - templateType: TemplateType - partialPath: string - ## The current `partial` path - excludes: seq[string] - - ImportError* = object of CatchableError - -const - ImportErrorNotFound = "Cannot import \"$1\". File not found" - ImportPartialSelf = "\"$1\" cannot import itself" - ImportCircularError = "Circular import of $1" - -proc hasError*[I: Importer](p: var I): bool = - result = p.error.len != 0 - -proc setError*[I: Importer](p: var I, msg: string, path: string) = - p.error = msg - p.error_line = p.current.line - p.error_column = p.current.col - if p.sources.hasKey(path): - p.error_trace = p.sources[path] - -proc getError*[I: Importer](p: var I): string = - result = p.error - -proc getErrorColumn*[I: Importer](p: var I): int = - result = p.error_column - -proc getErrorLine*[I: Importer](p: var I): int = - result = p.error_line - -proc getFullCode*[I: Importer](p: var I): string = - result = $p.rope - -template jump[I: Importer](p: var I, offset = 1) = - var i = 0 - while offset != i: - p.current = p.next - p.next = p.lex.getToken() - inc i - -template loadCode(p: var Importer, indent: int) = - var filepath = p.current.value - filepath = if not endsWith(filepath, ".timl"): filepath & ".timl" else: filepath - let dirpath = parentDir(p.currentFilePath) - let path = p.engine.getPathDir("partials") & "/" & filepath - if p.sources.hasKey(path): - # if partial has already been loaded once, then get it - # from memory table, instead of calling `readFile` again - p.partials[p.current.line] = (indent, path) - else: - if not fileExists(path): - p.setError(ImportErrorNotFound % [filepath], filepath) - else: - if path == p.currentFilePath: - p.setError(ImportPartialSelf % [filepath], filepath) - break - elif path in p.excludes: - p.setError(ImportCircularError % [path], filepath) - break - var excludeCirculars = deduplicate(concat(p.excludes, @[path, p.currentFilePath])) - var importResolver = resolve(readFile(path), path, p.engine, p.templateType, excludeCirculars) - if importResolver.hasError(): - raise newException(ImportError, importResolver.getError()) - p.sources[path] = importResolver.getFullCode() - p.partials[p.current.line] = (indent, path) - getTemplateByPath(p.engine, path).addDependentView(p.currentFilePath) - -template resolveChunks(p: var Importer) = - if p.current.kind == tkInclude: - let indent = p.current.col - if p.next.kind != tkString: - p.setError "Invalid import statement missing file path.", p.currentFilePath - break - jump p - loadCode(p, indent) - elif p.current.kind == tkView and p.templateType != Layout: - p.setError("Trying to load a view inside a $1" % [$p.templateType], p.currentFilePath) - break - -proc resolve*(viewCode, currentFilePath: string, - engine: TimEngine, templateType: TemplateType, - excludes: seq[string] = @[]): Importer = - ## Resolve ``@include`` statements - var p = Importer(engine: engine, - lex: Lexer.init(viewCode), - currentFilePath: currentFilePath, - templateType: templateType, - excludes: excludes) - p.current = p.lex.getToken() - p.next = p.lex.getToken() - while p.error.len == 0 and p.current.kind != tkEof: - p.resolveChunks() - jump p - if p.error.len == 0: - var sourceStream = newStringStream(viewCode) - var lineno = 1 - for line in lines(sourceStream): - if p.partials.hasKey(lineno): - let path: SourcePath = p.partials[lineno].source - let code: SourceCode = p.sources[path] - let indentSize = p.partials[lineno].indentSize - p.rope.add indent(code, indentSize) - p.rope.add "\n" - else: - p.rope.add line & "\n" - inc lineno - sourceStream.close() - result = p \ No newline at end of file diff --git a/src/timpkg/engine/tokens.nim b/src/timpkg/engine/tokens.nim deleted file mode 100644 index b632b43..0000000 --- a/src/timpkg/engine/tokens.nim +++ /dev/null @@ -1,384 +0,0 @@ -# A blazing fast, cross-platform, multi-language -# template engine and markup language written in Nim. -# -# Made by Humans from OpenPeeps -# (c) George Lemon | LGPLv3 License -# https://github.com/openpeeps/tim - -import toktok - -handlers: - proc handleVarFmt(lex: var Lexer, kind: TokenKind) = - lexReady lex - inc lex.bufpos - while true: - if lex.hasLetters(lex.bufpos): - add lex.token, lex.buf[lex.bufpos] - inc lex.bufpos - elif lex.hasNumbers(lex.bufpos): - add lex.token, lex.buf[lex.bufpos] - inc lex.bufpos - else: break - lex.setToken kind - - proc handleCalls(lex: var Lexer, kind: TokenKind) = - template collectSnippet(tkind: TokenKind) = - while true: - case lex.buf[lex.bufpos] - of EndOfFile: - lex.setError("EOF reached before closing @end") - return - of '@': - if lex.next("end"): - lex.kind = tkind - lex.token = lex.token.unindent(pos + 2) - inc lex.bufpos, 4 - break - else: - add lex - else: - add lex - lexReady lex - if lex.next("js"): - # setLen(lex.token, 0) - let pos = lex.getColNumber(lex.bufpos) - inc lex.bufpos, 3 - collectSnippet(tkJS) - elif lex.next("sass"): - setLen(lex.token, 0) - inc lex.bufpos, 5 - # k = tkSass - elif lex.next("yaml"): - setLen(lex.token, 0) - inc lex.bufpos, 5 - # k = tkYaml - elif lex.next("json"): - setLen(lex.token, 0) - inc lex.bufpos, 5 - # k = tkJson - elif lex.next("include"): - lex.setToken tkInclude, 8 - elif lex.next("view"): - lex.setToken tkView, 5 - elif lex.next("wasm"): - let pos = lex.getColNumber(lex.bufpos) - inc lex.bufpos, 5 - if lex.buf[lex.bufpos] == '#': - var ident: string - while true: - if lex.buf[lex.bufpos] in Whitespace: - break - inc lex.bufpos - add ident, lex.buf[lex.bufpos] - lex.attr.add(ident.strip()) - collectSnippet(tkWasm) - else: - lex.setError("Invalid Runtime snippet missing ID attribute") - return - else: - inc lex.bufpos - setLen(lex.token, 0) - while true: - if lex.hasLetters(lex.bufpos) or lex.hasNumbers(lex.bufpos): - add lex.token, lex.buf[lex.bufpos] - inc lex.bufpos - else: - dec lex.bufpos - break - lex.setToken tkCall - - # proc handleSnippets(lex: var Lexer, kind: TokenKind) = - # lex.startPos = lex.getColNumber(lex.bufpos) - # var k = tkJs - # if lex.next("javascript"): - # setLen(lex.token, 0) - # inc lex.bufpos, 11 - # elif lex.next("sass"): - # setLen(lex.token, 0) - # inc lex.bufpos, 5 - # k = tkSass - # elif lex.next("yaml"): - # setLen(lex.token, 0) - # inc lex.bufpos, 5 - # k = tkYaml - # elif lex.next("json"): - # setLen(lex.token, 0) - # inc lex.bufpos, 5 - # k = tkJson - # else: - # lex.setError("Unknown snippet. Tim knows about `js`|`javascript` or `sass`") - # return - # while true: - # case lex.buf[lex.bufpos] - # of '`': - # if lex.next("``"): - # lex.kind = k - # inc lex.bufpos, 3 - # break - # else: - # add(lex) - # of EndOfFile: - # lex.setError("EOF reached before end of snippet") - # return - # else: - # add lex.token, lex.buf[lex.bufpos] - # inc lex.bufpos - - proc handleCustomIdent(lex: var Lexer, kind: TokenKind) = - ## Handle variable declarations based the following char sets - ## ``{'a'..'z', 'A'..'Z', '_', '-'}`` and ``{'0'..'9'}`` - lexReady lex - inc lex.bufpos - while true: - if lex.hasLetters(lex.bufpos): - add lex.token, lex.buf[lex.bufpos] - inc lex.bufpos - elif lex.hasNumbers(lex.bufpos): - add lex.token, lex.buf[lex.bufpos] - inc lex.bufpos - else: - dec lex.bufpos - break - lex.setToken kind - -registerTokens defaultSettings: - a = "a" - abbr = "abbr" - acronym = "acronym" - address = "address" - applet = "applet" - area = "area" - article = "article" - aside = "aside" - audio = "audio" - bold = "b" - base = "base" - basefont = "basefont" - bdi = "bdi" - bdo = "bdo" - big = "big" - blockquote = "blockquote" - body = "body" - br = "br" - button = "button" - divide = '/': - comment = '/' .. EOL - canvas = "canvas" - caption = "caption" - center = "center" - cite = "cite" - code = "code" - col = "col" - colgroup = "colgroup" - data = "data" - datalist = "datalist" - dD = "dd" - del = "del" - details = "details" - dFN = "dfn" - dialog = "dialog" - dir = "dir" - `div` = "div" - doctype = "doctype" - dl = "dl" - dt = "dt" - em = "em" - embed = "embed" - fieldset = "fieldset" - figcaption = "figcaption" - figure = "figure" - font = "font" - footer = "footer" - form = "form" - frame = "frame" - frameset = "frameset" - h1 = "h1" - h2 = "h2" - h3 = "h3" - h4 = "h4" - h5 = "h5" - h6 = "h6" - head = "head" - header = "header" - hr = "hr" - html = "html" - italic = "i" - iframe = "iframe" - img = "img" - input = "input" - ins = "ins" - kbd = "kbd" - label = "label" - legend = "legend" - li = "li" - link = "link" - main = "main" - map = "map" - mark = "mark" - meta = "meta" - meter = "meter" - nav = "nav" - noframes = "noframes" - noscript = "noscript" - `object` = "object" - ol = "ol" - optgroup = "optgroup" - option = "option" - output = "output" - paragraph = "p" - param = "param" - pre = "pre" - progress = "progress" - quotation = "q" - rp = "rp" - rt = "rt" - ruby = "ruby" - strike = "s" - samp = "samp" - script = "script" - section = "section" - select = "select" - small = "small" - source = "source" - span = "span" - strike_Long = "strike" - strong = "strong" - style = "style" - sub = "sub" - summary = "summary" - sup = "sup" - - svg = "svg" - svg_Animate = "animate" - svg_AnimateMotion = "animateMotion" - svg_AnimateTransform = "animateTransform" - svg_Circle = "circle" - svg_ClipPath = "clipPath" - svg_Defs = "defs" - svg_Desc = "desc" - svg_Discard = "discard" - svg_Ellipse = "ellipse" - svg_Fe_Blend = "feBlend" - svg_Fe_ColorMatrix = "feColorMatrix" - svg_Fe_ComponentTransfer = "feComponentTransfer" - svg_Fe_Composite = "feComposite" - svg_Fe_ConvolveMatrix = "feConvolveMatrix" - svg_Fe_DiffuseLighting = "feDiffuseLighting" - svg_Fe_DisplacementMap = "feDisplacementMap" - svg_Fe_DistantLight = "feDistantLight" - svg_Fe_DropShadow = "feDropShadow" - svg_Fe_Flood = "feFlood" - svg_Fe_FuncA = "feFuncA" - svg_Fe_FuncB = "feFuncB" - svg_Fe_FuncG = "feFuncG" - svg_Fe_FuncR = "feFuncR" - svg_Fe_GaussianBlur = "feGaussianBlur" - svg_Fe_Image = "feImage" - svg_Fe_Merge = "feMerge" - svg_Fe_Morphology = "feMorphology" - svg_Fe_Offset = "feOffset" - svg_Fe_PointLight = "fePointLight" - svg_Fe_SpecularLighting = "feSpecularLighting" - svg_Fe_SpotLight = "feSpotLight" - svg_Fe_Title = "feTitle" - svg_Fe_Turbulence = "feTurbulence" - svg_Filter = "filter" - svg_foreignObject = "foreignObject" - svg_G = "g" - svg_Hatch = "hatch" - svg_HatchPath = "hatchpath" - svg_Image = "image" - svg_Line = "line" - svg_LinearGradient = "linearGradient" - svg_Marker = "marker" - svg_Mask = "mask" - svg_Metadata = "metadata" - svg_Mpath = "mpath" - svg_Path = "path" - svg_Pattern = "pattern" - svg_Polygon = "polygon" - svg_Polyline = "polyline" - svg_RadialGradient = "radialGradient" - svg_Rect = "rect" - svg_Set = "set" - svg_Stop = "stop" - svg_Switch = "switch" - svg_Symbol = "symbol" - svg_Text = "text" - svg_TextPath = "textpath" - svg_TSpan = "tspan" - svg_Use = "use" - svg_View = "view" - - table = "table" - tbody = "tbody" - td = "td" - `template` = "template" - textarea = "textarea" - tfoot = "tfoot" - tH = "th" - thead = "thead" - time = "time" - title = "title" - tR = "tr" - track = "track" - tT = "tt" - underline = "u" - uL = "ul" - `var` = "var" - video = "video" - wbr = "wbr" - attr # a tkIdentifier followed by `=` becomes tkAttr - js - sass - yaml - json - # snippet = tokenize(handleSnippets, '`') - lc = '{' - rc = '}' - lp = '(' - rp = ')' - lb = '[' - rb = ']' - dot = '.' - id = '#' - assign = '=': - eq = '=' - colon = ':' - comma = ',' - gt = '>': - gte = '=' - lt = '<': - lte = '=' - amp = '&' - variable = tokenize(handleCustomIdent, '$') - safeVariable = tokenize(handleCustomIdent, '%') - `if` = "if" - `elif` = "elif" - `else` = "else" - sif = '?' # short hand `if` statement - selse = '|' # short hand `else` statement - `and` = "and" - `for` = "for" - `in` = "in" - `or` = "or" - `bool` = ["true", "false"] - `not` = '!': - ne = '=' - at = tokenize(handleCalls, '@') - `include` - view - `mixin` - call - # `end` - runtime - wasm - plus = '+' - minus = '-' - multi = '*' - `defer` = "defer" - typeBool = "bool" - typeInt = "int" - typeString = "string" - typeFloat = "float" - none diff --git a/src/timpkg/engine/utils.nim b/src/timpkg/engine/utils.nim deleted file mode 100644 index 7ac3800..0000000 --- a/src/timpkg/engine/utils.nim +++ /dev/null @@ -1,5 +0,0 @@ -proc malloc_trim*(size: csize_t): cint {.importc, varargs, header: "malloc.h", discardable.} - -template freem*(obj: untyped) = - reset(obj) - discard malloc_trim(sizeof(obj).csize_t) \ No newline at end of file diff --git a/tests/examples/storage/.gitkeep b/tests/app/storage/.gitkeep similarity index 100% rename from tests/examples/storage/.gitkeep rename to tests/app/storage/.gitkeep diff --git a/tests/app/templates/layouts/base.timl b/tests/app/templates/layouts/base.timl new file mode 100644 index 0000000..9c2136f --- /dev/null +++ b/tests/app/templates/layouts/base.timl @@ -0,0 +1,7 @@ +html + head + meta charset="utf-8" + meta name="viewport" content="width=device-width, initial-scale=1" + title: "Tim Engine is Awesome!" + body + @view \ No newline at end of file diff --git a/tests/app/templates/views/index.timl b/tests/app/templates/views/index.timl new file mode 100644 index 0000000..e8ec478 --- /dev/null +++ b/tests/app/templates/views/index.timl @@ -0,0 +1,2 @@ +div.container > div.row > div.col-12 + h1: "Hello!" \ No newline at end of file diff --git a/tests/config.nims b/tests/config.nims index 981a06e..3bb69f8 100644 --- a/tests/config.nims +++ b/tests/config.nims @@ -1,3 +1 @@ -switch "path", "$projectDir/../src" -switch "threads", "on" -switch "define", "nimOldCaseObjects" # fix object case trasition errors from msgpack4nim \ No newline at end of file +switch("path", "$projectDir/../src") \ No newline at end of file diff --git a/tests/examples/templates/layouts/base.timl b/tests/examples/templates/layouts/base.timl deleted file mode 100644 index dedee79..0000000 --- a/tests/examples/templates/layouts/base.timl +++ /dev/null @@ -1,6 +0,0 @@ -html - head - meta charset="utf-8" - title: "This is Tim" - body - @view \ No newline at end of file diff --git a/tests/examples/templates/views/index.timl b/tests/examples/templates/views/index.timl deleted file mode 100644 index 9656423..0000000 --- a/tests/examples/templates/views/index.timl +++ /dev/null @@ -1,3 +0,0 @@ -div.container > div.row > div.col-12 - h1.fw-bold: $this.headline - p.lead: "A high-performance template engine & markup language" \ No newline at end of file diff --git a/tests/examples/templates/views/static.timl b/tests/examples/templates/views/static.timl deleted file mode 100644 index c597ecd..0000000 --- a/tests/examples/templates/views/static.timl +++ /dev/null @@ -1 +0,0 @@ -h3: "static things don't need JIT" \ No newline at end of file diff --git a/tests/test1.nim b/tests/test1.nim index 76bd106..8649013 100644 --- a/tests/test1.nim +++ b/tests/test1.nim @@ -1,109 +1,11 @@ -import std/[unittest, json, htmlparser, - xmltree, strtabs, sequtils] -import tim +import std/unittest +import ../src/tim -Tim.init( - source = "./examples/templates", - output = "./examples/storage/templates", - minified = false, - indent = 2 -) +var t = newTim("./app/templates", "./app/storage", + currentSourcePath(), minify = false, indent = 2) -Tim.setData(%*{ - "appName": "My application", - "production": false, - "keywords": ["template-engine", "html", "tim", "compiled", "templating"], -}) +test "precompile": + t.precompile(flush = true, waitThread = false) -test "can init": - check Tim.templatesExists == true - check Tim.getIndent == 2 - check Tim.shouldMinify == false - -test "can precompile": - Tim.precompile() - -test "can render (file)": - let x = Tim.render("index", data = %*{ - "headline": "Tallulah bottoms recently departed!" - }) - let - output = x.parseHtml - h1 = output.findAll("h1").toSeq[0] - p = output.findAll("p").toSeq[0] - - check h1.attrs.hasKey("class") == true - check h1.attrs["class"] == "fw-bold" - check h1.attrsLen == 1 - check h1.innerText == "Tallulah bottoms recently departed!" - - check p.attrs.hasKey("class") == true - check p.attrs["class"] == "lead" - check p.attrsLen == 1 - check p.innerText == "A high-performance template engine & markup language" - -test "can render static (file)": - let x = Tim.render("static") - -test "can render (code)": - var output = tim2html("div > span: \"Hello\"", true) - check(output == "
Hello
") - - output = tim2html(""" -a.text-link href="https://openpeeps.github.io/tim/": "API Reference" - """) - let - xmlcode = output.parseHtml - a = xmlcode.findAll("a").toSeq[0] - check a.attrsLen == 2 - check a.attrs["class"] == "text-link" - -test "can render loop (code)": - var output = tim2html(""" -for $x in $this.list: - span: $x - """, data = %*{ - "list": ["one", "two", "three"] - }) - check output.parseHtml.findAll("span").len == 3 - -test "can render conditionals (code)": - var output = tim2html(""" -if $this.hello == "world" - span: "Hello World" - -if $this.enabled != true: - span: "Disabled" -else: - span: "Enabled" - -if $this.counter < 120: - span: "less than 120" -elif $this.counter >= 120: - span: "greater or equal" -else: - span > u: "nothing here" - """, data = %*{ - "hello": "world", - "enabled": true, - "counter": 120 - }) - -test "can render element with multi-line attributes": - let output = tim2html(""" -main > div - button#nav-advanced-tab.nav-link - data-bs-toggle="pill" - data-bs-target="#nav-advanced" - type="button" - role="tab" - aria-controls="pills-home" - aria-selected="true": "Advanced" - a href="#": "Hello" - """, minify = true) - let - xmlOutput = output.parseHtml - btn = xmlOutput.findAll("button").toSeq[0] - a = xmlOutput.findAll("a").toSeq[0] - check btn.attrsLen == 8 - check a.attrsLen == 1 \ No newline at end of file +test "render index": + let html = t.render("index") \ No newline at end of file diff --git a/tim.nimble b/tim.nimble index 60ebe8d..29e3676 100644 --- a/tim.nimble +++ b/tim.nimble @@ -2,36 +2,34 @@ version = "0.1.0" author = "George Lemon" -description = "High-performance, compiled template engine inspired by Emmet syntax" +description = "A new awesome nimble package" license = "MIT" srcDir = "src" installExt = @["nim"] bin = @["tim"] -binDir = "bin" + # Dependencies -requires "nim >= 1.6.0" -requires "pkginfo" +requires "nim >= 2.0.0" requires "toktok" requires "jsony" -requires "watchout" -requires "nyml" -requires "kapsis" -requires "denim" -requires "msgpack4nim#head" - -task tests, "Run tests": - exec "testament p 'tests/*.nim'" - -task dev, "Dev build": - exec "nimble build" - -task prod, "Release build": - exec "nimble build -d:release" - -task emsdk, "Build a .wasm via Emscripten": - exec "nim c -d:emscripten src/tim.nim" - -task napi, "Compile Tim via NAPI": - exec "denim build src/tim.nim --cmake --release --yes" \ No newline at end of file +requires "importer" +requires "watchout#head" +requires "kapsis#head" +requires "denim#head" +requires "checksums" +requires "flatty" +requires "supersnappy" +requires "stashtable" +# requires "httpx" +# requires "websocketx" + +task node, "Build a NODE addon": + exec "denim build src/tim.nim --cmake --yes" + +task example, "Build example": + exec "nim c -d:timHotCode --threads:on --mm:arc -o:./bin/app example/app.nim" + +task pexample, "Build example": + exec "nim c -d:timHotCode -d:danger --passC:-flto --threads:on --mm:arc -o:./bin/app example/app.nim" \ No newline at end of file