A => .gitignore +1 -0
A => LICENSE +19 -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.
A => README.md +37 -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 $}
+<html>
+ <body>
+ {{ (string/repeat "<>" n) # HTML escaped }}
+ <ul>
+ {% (each x (range n) (print "<li>" x " " (args :a) "</li>")) # No auto-print %}
+ </ul>
+ {- (string/repeat "<span>1</span>" n) # Not HTML escaped -}
+ </body>
+</html>
+```
+
+main.janet
+```
+(import temple)
+(temple/add-loader)
+
+(import ./foo :as foo)
+(foo/render :a "hello")
+```
A => project.janet +10 -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"])
A => temple.janet +139 -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))
A => test/basic.janet +33 -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}
+```
+<html>
+ 6
+</html>
+```)
+
+(check-template hop/render-dict {:a 1 :b 2}
+```
+<html>
+ 6
+</html>
+```)
A => test/example.janet +4 -0
@@ 1,4 @@
+(import ../temple :as temple)
+(temple/add-loader)
+(import ./templates/foo :as foo)
+(foo/render)
A => test/templates/foo.temple +10 -0
@@ 1,10 @@
+{$ (def n 20) # Run at template compile time $}
+<html>
+ <body>
+ {{ (string/repeat "<>" n) # HTML escaped }}
+ <ul>
+ {% (each x (range n) (print "<li>" x " " (args :a) "</li>")) # No auto-print %}
+ </ul>
+ {- (string/repeat "<span></span>" n) # No HTML escape -}
+ </body>
+</html>
A => test/templates/hi.temple +4 -0
@@ 1,4 @@
+{$ (defn myfn [x] (+ x x)) $}
+<html>
+ {{ (myfn (length (range (+ (args :a) (args :b))))) }}
+</html>
A => test/templates/hop.temple +2 -0
@@ 1,2 @@
+{$ (import ./hi :as hi) $}
+{% (hi/render-dict args) %}