From e7d6a355857991a00525a237928a466a781d94f5 Mon Sep 17 00:00:00 2001 From: Calvin Rose Date: Sun, 12 Jan 2020 22:37:25 -0600 Subject: [PATCH] First commit. --- .gitignore | 1 + LICENSE | 19 ++++++ README.md | 37 ++++++++++ project.janet | 10 +++ temple.janet | 139 ++++++++++++++++++++++++++++++++++++++ test/basic.janet | 33 +++++++++ test/example.janet | 4 ++ test/templates/foo.temple | 10 +++ test/templates/hi.temple | 4 ++ test/templates/hop.temple | 2 + 10 files changed, 259 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 project.janet create mode 100644 temple.janet create mode 100644 test/basic.janet create mode 100644 test/example.janet create mode 100644 test/templates/foo.temple create mode 100644 test/templates/hi.temple create mode 100644 test/templates/hop.temple diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1320f90 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +site diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..90add8f --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Calvin Rose and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2d4bc6 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Temple + +HTML templates for [Janet](https://janet-lang.org). + +Simplified version of Mendoza's template system that is cleaner and easier to use. +Templates can be used recursively, and output is printed via `print`, so goes to +`(dyn :out)`. + +Expands on the mendoza templates with the `{=` ... `-}` brackets, which do non-escaped +substitution, so temple can be used for formats besides HTML. +Also exposes the `escape` function inside templates for HTML escaping +if you want to manually print to template output. + +## Example + +foo.temple +``` +{$ (def n 20) # Run at template compile time $} + + + {{ (string/repeat "<>" n) # HTML escaped }} + + {- (string/repeat "1" n) # Not HTML escaped -} + + +``` + +main.janet +``` +(import temple) +(temple/add-loader) + +(import ./foo :as foo) +(foo/render :a "hello") +``` diff --git a/project.janet b/project.janet new file mode 100644 index 0000000..5451c82 --- /dev/null +++ b/project.janet @@ -0,0 +1,10 @@ +(declare-project + :name "temple" + :author "Calvin Rose" + :license "MIT" + :version "0.0.1" + :url "https://github.com/bakpakin/temple" + :repo "git+https://github.com/bakpakin/temple") + +(declare-source + :source ["temple.janet"]) diff --git a/temple.janet b/temple.janet new file mode 100644 index 0000000..5bb306c --- /dev/null +++ b/temple.janet @@ -0,0 +1,139 @@ +### +### temple.janet +### Copyright © Calvin Rose 2020 +### + +(defmacro- defenv + "Define a module inline as if returned by require." + [what & forms] + (def env (make-env)) + (each f forms + (resume (fiber/setenv (coro (eval f)) env))) + ~(def ,what ',env)) + +(defenv base-env + # Define forms available inside the temple DSL here + (def- escape-peg + (peg/compile + ~(% (any (+ (* "&" (constant "&")) + (* "\"" (constant """)) + (* "<" (constant "<")) + (* ">" (constant ">")) + (* "'" (constant "'")) + '1))))) + (defn escape [x] + (in (peg/match escape-peg (string x)) 0))) + +(defn create + "Compile a template string into a function. Optionally + provide a location where the source is from to improve debugging. Returns + the template function." + [source &opt where] + + (default where source) + (def env (table/setproto @{} base-env)) + + # Inherit dyns + (let [current-env (fiber/getenv (fiber/current))] + (loop [[k v] :pairs current-env :when (keyword? k)] + (put env k v))) + + # State for compilation machine + (def p (parser/new)) + (def forms @[]) + + (defn compile-time-chunk + "Eval the capture straight away during compilation. Use for imports, etc." + [chunk] + (defn do-in-env [] (eval-string chunk)) + (def f (fiber/new do-in-env)) + (fiber/setenv f env) + (resume f) + true) + + (defn parse-chunk + "Parse a string and push produced values to forms." + [chunk] + (parser/consume p chunk) + (while (parser/has-more p) + (array/push forms (parser/produce p)))) + + (defn code-chunk + "Parse all the forms in str and insert them into the template." + [str] + (parse-chunk str) + (if (= :error (parser/status p)) + (error (parser/error p))) + true) + + (defn sub-chunk + "Same as code-chunk, but results in sending code to the buffer." + [str] + (code-chunk + (string "\n(prin (escape (do " str "\n))) "))) + + (defn raw-chunk + "Same as code-chunk, but results in sending code to the buffer." + [str] + (code-chunk + (string "\n(prin (do " str "\n)) "))) + + (defn string-chunk + "Insert string chunk into parser" + [str] + (parse-chunk "\n") + (parser/insert p ~(,prin ,str)) + true) + + # Run peg + (def grammar + ~{:code-chunk (* "{%" (drop (cmt '(any (if-not "%}" 1)) ,code-chunk)) "%}") + :compile-time-chunk (* "{$" (drop (cmt '(any (if-not "$}" 1)) ,compile-time-chunk)) "$}") + :sub-chunk (* "{{" (drop (cmt '(any (if-not "}}" 1)) ,sub-chunk)) "}}") + :raw-chunk (* "{-" (drop (cmt '(any (if-not "-}" 1)) ,raw-chunk)) "-}") + :main-chunk (drop (cmt '(any (if-not (+ "{$" "{{" "{%" "{-") 1)) ,string-chunk)) + :main (any (+ :compile-time-chunk :raw-chunk :code-chunk :sub-chunk :main-chunk (error "")))}) + (def did-match (peg/match grammar source)) + + # Check errors in template and parser + (unless did-match (error "invalid template syntax")) + (parse-chunk "\n") + (parser/eof p) + (case (parser/status p) + :error (error (parser/error p))) + + # Make ast from forms + (def ast ~(fn temple-template [args] + ,;forms + nil)) + + (def ctor (compile ast env (string where))) + (if-not (function? ctor) + (error (string "could not compile template: " (string/format "%p" ctor)))) + + (let [f (fiber/new ctor :e)] + (fiber/setenv f env) + (def res (resume f)) + (case res + :error (error res) + res))) + +# +# Module loading +# + +(defn- loader + [path &] + (with-dyns [:current-file path] + (let [tmpl (create (slurp path) path)] + @{'render @{:doc "Main template function." + :value (fn render [&keys args] (tmpl args)) } + 'render-dict @{:doc "Template function, but pass arguments as a dictionary." + :value tmpl}}))) + +(defn add-loader + "Adds the custom template loader to Janet's module/loaders and + update module/paths." + [] + (put module/loaders :temple loader) + (module/add-paths ".temple" :temple)) diff --git a/test/basic.janet b/test/basic.janet new file mode 100644 index 0000000..a80572c --- /dev/null +++ b/test/basic.janet @@ -0,0 +1,33 @@ +(import ../temple :as temple) +(temple/add-loader) + +(defn check-template + [template args expected] + (def buf @"") + (with-dyns [:out buf] + (template args) + (def sbuf (string/trim (string buf))) + (if (= sbuf expected) + (eprint "pass") + (do + (eprint) + (eprint "fail - expected " (describe expected) + ", got " (describe sbuf)) + (eprint))))) + +(import ./templates/hi :as hi) +(import ./templates/hop :as hop) + +(check-template hi/render-dict {:a 1 :b 2} +``` + + 6 + +```) + +(check-template hop/render-dict {:a 1 :b 2} +``` + + 6 + +```) diff --git a/test/example.janet b/test/example.janet new file mode 100644 index 0000000..07f1caf --- /dev/null +++ b/test/example.janet @@ -0,0 +1,4 @@ +(import ../temple :as temple) +(temple/add-loader) +(import ./templates/foo :as foo) +(foo/render) diff --git a/test/templates/foo.temple b/test/templates/foo.temple new file mode 100644 index 0000000..59b96f3 --- /dev/null +++ b/test/templates/foo.temple @@ -0,0 +1,10 @@ +{$ (def n 20) # Run at template compile time $} + + + {{ (string/repeat "<>" n) # HTML escaped }} + + {- (string/repeat "" n) # No HTML escape -} + + diff --git a/test/templates/hi.temple b/test/templates/hi.temple new file mode 100644 index 0000000..1c32d4b --- /dev/null +++ b/test/templates/hi.temple @@ -0,0 +1,4 @@ +{$ (defn myfn [x] (+ x x)) $} + + {{ (myfn (length (range (+ (args :a) (args :b))))) }} + diff --git a/test/templates/hop.temple b/test/templates/hop.temple new file mode 100644 index 0000000..4353c70 --- /dev/null +++ b/test/templates/hop.temple @@ -0,0 +1,2 @@ +{$ (import ./hi :as hi) $} +{% (hi/render-dict args) %} -- 2.45.2