~bakpakin/temple

e7d6a355857991a00525a237928a466a781d94f5 — Calvin Rose 1 year, 1 month ago
First commit.
A  => .gitignore +1 -0
@@ 1,1 @@
site

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 "&amp;"))
                  (* "\"" (constant "&quot;"))
                  (* "<" (constant "&lt;"))
                  (* ">" (constant "&gt;"))
                  (* "'" (constant "&#39;"))
                  '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) %}