├── .gitignore
├── TODO
├── examples
├── package.lisp
├── i18n.html
├── filters.html
├── dot-syntax.html
├── many.html
├── dot-syntax-2.html
├── include.html
├── escaping.html
├── ex1.html
├── with-output-whitespace.html
├── comments.html
├── control.html
└── inheritance.html
├── ten.i18n.gettext.asd
├── ten.i18n.cl-locale.asd
├── i18n.gettext.lisp
├── ten.tests.asd
├── .travis.yml
├── ten.asd
├── ten.lisp
├── i18n.cl-locale.lisp
├── LICENSE
├── i18n.lisp
├── ten.examples.asd
├── asdf.lisp
├── package.lisp
├── ten-mode.el
├── tests.lisp
├── parser.lisp
├── template.lisp
├── compiler.lisp
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.fasl
3 | examples/*.lisp
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | - comments
2 | - configurable parser delimiters
3 |
--------------------------------------------------------------------------------
/examples/package.lisp:
--------------------------------------------------------------------------------
1 | (defpackage ten/examples
2 | (:use :cl :ten))
3 |
--------------------------------------------------------------------------------
/examples/i18n.html:
--------------------------------------------------------------------------------
1 | {% template i18n-msg () (message) %}
2 |
3 | {{ _ message }}
4 |
5 | {% end %}
6 |
--------------------------------------------------------------------------------
/examples/filters.html:
--------------------------------------------------------------------------------
1 | {% template filters1 () (str) %}
2 | {{str | string-upcase}}
3 | {% end %}
4 |
5 | {% template filters2 () (str) %}
6 | {{str | string-upcase | string-trim '(#\%) _}}
7 | {% end %}
8 |
--------------------------------------------------------------------------------
/examples/dot-syntax.html:
--------------------------------------------------------------------------------
1 | {% template dot-syntax () (user) %}
2 |
3 |
4 |
5 |
6 | Hello {{ user.firstname }} {{user.lastname}}
7 |
8 |
9 | {% end %}
10 |
--------------------------------------------------------------------------------
/examples/many.html:
--------------------------------------------------------------------------------
1 | {% ten:template util1 () (foo) %}
2 |
3 | {{ foo }}
4 |
5 | {% end %}
6 |
7 | {% ten:template util2 () (bar) %}
8 |
9 | {{ bar }}
10 |
11 | {% end %}
12 |
--------------------------------------------------------------------------------
/examples/dot-syntax-2.html:
--------------------------------------------------------------------------------
1 | {% template dot-syntax-2 () (user) %}
2 | {% access:with-dot () %}
3 |
4 |
5 |
6 |
7 |
8 | Hello {{ user.firstname | string-capitalize }} {{user.lastname | string-capitalize }}
9 |
10 |
11 |
12 | {% end %}
13 | {% end %}
14 |
--------------------------------------------------------------------------------
/ten.i18n.gettext.asd:
--------------------------------------------------------------------------------
1 | (asdf:defsystem #:ten.i18n.gettext
2 | :description "i18n for TEN Common Lisp Template System using gettext"
3 | :author "Mariano Montone "
4 | :license "MIT"
5 | :version "0.0.1"
6 | :serial t
7 | :depends-on (:ten :gettext)
8 | :components ((:file "i18n.gettext")))
9 |
--------------------------------------------------------------------------------
/ten.i18n.cl-locale.asd:
--------------------------------------------------------------------------------
1 | (asdf:defsystem #:ten.i18n.cl-locale
2 | :description "i18n for TEN Common Lisp Template System using cl-locale"
3 | :author "Mariano Montone "
4 | :license "MIT"
5 | :version "0.0.1"
6 | :serial t
7 | :depends-on (:ten :cl-locale)
8 | :components ((:file "i18n.cl-locale")))
9 |
--------------------------------------------------------------------------------
/examples/include.html:
--------------------------------------------------------------------------------
1 | {% template include () (xs) %}
2 |
3 |
4 |
5 |
6 |
7 | {% loop for x in xs do %}
8 | {{ item x }}
9 | {% end %}
10 |
11 |
12 |
13 | {% end %}
14 |
15 | {% template item () (x) %}
16 | {{x}}
17 | {% end %}
18 |
--------------------------------------------------------------------------------
/i18n.gettext.lisp:
--------------------------------------------------------------------------------
1 | (in-package :ten/template)
2 |
3 | (defvar *gettext-domain* nil)
4 |
5 | (defmethod backend-translate ((backend (eql :gettext)) string language &rest args)
6 | (apply #'format-translation
7 | (gettext:gettext* string *gettext-domain* nil (string-downcase (string language)))
8 | args))
9 |
10 | (setf *translation-backend* :gettext)
11 |
--------------------------------------------------------------------------------
/ten.tests.asd:
--------------------------------------------------------------------------------
1 | (asdf:defsystem #:ten.tests
2 | :description "Tests for TEN Common Lisp Template System"
3 | :author "Mariano Montone "
4 | :license "MIT"
5 | :version "0.0.1"
6 | :serial t
7 | :depends-on (:ten :ten.examples :fiveam)
8 | :components ((:file "tests"))
9 | :perform (asdf:test-op (op c)
10 | (uiop:symbol-call :ten/tests :run-tests)))
11 |
--------------------------------------------------------------------------------
/examples/escaping.html:
--------------------------------------------------------------------------------
1 | {% template escaping1 () () %}
2 | This is HTML code:
3 | {% begin-raw %}
4 |
5 | {% end %}
6 | {% end %}
7 |
8 | {% template escaping2 () (str) %}
9 | This is escaped: {{ str }}
10 | {% end %}
11 |
12 | {% template escaping3 () (html) %}
13 | This is HTML code:
14 | {{ html | raw}}
15 | {% end %}
16 |
17 | {% template escaping4 () (html) %}
18 | This is HTML code:
19 | {% begin-raw %}
20 | {{ html }}
21 | {% end %}
22 | {% end %}
23 |
--------------------------------------------------------------------------------
/examples/ex1.html:
--------------------------------------------------------------------------------
1 | {% template ex1 () (user enabled &key items) %}
2 |
3 |
4 |
5 |
6 |
7 | {{ user.name | string-capitalize }}
8 |
9 | {% if enabled %}
10 | Enabled
11 | {% else %}
12 | Disabled
13 | {% end %}
14 |
15 | {% when items %}
16 |
17 | {% loop for item in items do %}
18 | - {{ item }}
19 | {% end %}
20 |
21 | {% end %}
22 |
23 | {% when (not items) %}
24 | There are no items
25 | {% end %}
26 |
27 |
28 | {% end %}
29 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: common-lisp
2 |
3 | env:
4 | matrix:
5 | - LISP=sbcl
6 |
7 | install:
8 | # Install cl-travis
9 | - curl https://raw.githubusercontent.com/luismbo/cl-travis/master/install.sh | bash
10 |
11 | script:
12 | - cl -e '(ql:quickload :fiveam)'
13 | -e '(setf fiveam:*debug-on-error* t
14 | fiveam:*debug-on-failure* t)'
15 | -e '(setf *debugger-hook*
16 | (lambda (c h)
17 | (declare (ignore c h))
18 | (uiop:quit -1)))'
19 | -e '(ql:quickload :ten)'
20 | -e '(ql:quickload :ten.tests)'
21 | -e '(asdf:test-system :ten)'
22 |
23 | notifications:
24 | email:
25 | - marianomontone@gmail.com
26 |
--------------------------------------------------------------------------------
/ten.asd:
--------------------------------------------------------------------------------
1 | (asdf:defsystem "ten"
2 | :description "Template System for Common Lisp"
3 | :author "Mariano Montone "
4 | :license "MIT"
5 | :version "0.0.1"
6 | :homepage "https://github.com/mmontone/ten"
7 | :serial t
8 | :depends-on (:access :esrap :cl-who :split-sequence)
9 | :components ((:file "package")
10 | (:file "parser")
11 | (:file "template")
12 | (:file "compiler")
13 | (:file "asdf")
14 | (:file "i18n")
15 | (:file "ten"))
16 | :long-description
17 | #.(uiop:read-file-string
18 | (uiop:subpathname *load-pathname* "README.md"))
19 | :in-order-to ((asdf:test-op (asdf:test-op :ten.tests))))
20 |
--------------------------------------------------------------------------------
/examples/with-output-whitespace.html:
--------------------------------------------------------------------------------
1 | {% template with-output-whitespace-test-1 (:output-whitespace nil) (data) %}
2 | {% loop for item in data do %}
3 | {{item.car}} {{item.cdr}}
4 | {% end %}
5 | {% end %}
6 |
7 | {% template with-output-whitespace-test-2 (:output-whitespace nil) (data) %}
8 | {% loop for item in data do %}
9 | {% with-output-whitespace T %}{{item.car}} {{item.cdr}}
10 | {% end %}
11 | {% end %}
12 | {% end %}
13 |
14 | {% template with-output-whitespace-test-3 (:output-whitespace nil) (data) %}
15 | {% loop for item in data do %}
16 | {% with-output-whitespace T %}{{item.car}} {{item.cdr}}
17 | {% end %}
18 | {% end %}
19 | {% loop for item in data do %}
20 | {{item.car}} {{item.cdr}}
21 | {% end %}
22 | {% end %}
23 |
--------------------------------------------------------------------------------
/ten.lisp:
--------------------------------------------------------------------------------
1 | (in-package #:ten)
2 |
3 | (defun expand-template (string-or-pathname &optional (package-name ten/compiler:*template-package*))
4 | "Expand a template to Lisp code. Useful for debugging."
5 | (ten/compiler:compile-template
6 | (ten/parser:parse-template string-or-pathname)
7 | package-name))
8 |
9 | (defun compile-template (string-or-pathname &optional (package-name ten/compiler:*template-package*))
10 | "Compile a template. If a pathname is given, compiles the file content. Otherwise, compiles the given string."
11 | (let* ((expanded-template (expand-template string-or-pathname package-name)))
12 | (if (atom (first expanded-template))
13 | (eval expanded-template)
14 | (mapcar 'eval expanded-template)))
15 | t)
16 |
--------------------------------------------------------------------------------
/examples/comments.html:
--------------------------------------------------------------------------------
1 | {% template commented-ex1 () (user enabled &key items) %}
2 |
3 |
4 |
5 |
6 |
7 | {# Output the user name #}
8 | {{ user.name | string-capitalize }}
9 |
10 | {# Is user enabled?
11 |
12 | Check that user is enabled ...
13 |
14 | #}
15 |
16 | {% if enabled %}
17 | Enabled
18 | {% else %}
19 | Disabled
20 | {% end %}
21 |
22 | {# Output the list of items #}
23 |
24 | {% when items %}
25 |
26 | {% loop for item in items do %}
27 | - {{ item }}
28 | {% end %}
29 |
30 | {% end %}
31 |
32 | {# Message when there are no items #}
33 |
34 | {% when (not items) %}
35 | There are no items
36 | {% end %}
37 |
38 |
39 | {% end %}
40 |
--------------------------------------------------------------------------------
/i18n.cl-locale.lisp:
--------------------------------------------------------------------------------
1 | (in-package :ten/template)
2 |
3 | #-lispworks
4 | (defmethod backend-translate ((backend (eql :cl-locale)) string language &rest args)
5 | (let ((dictionary (locale:current-dictionary)))
6 | (when (not (arnesi:aand (not (eq language locale:*default-locale*))
7 | (gethash language dictionary)
8 | (gethash string arnesi:it)))
9 | (when *warn-on-untranslated-messages*
10 | (warn "TRANSLATION NOT GIVEN: ~A ~A" string language))
11 | (pushnew (cons string language) *untranslated-messages* :test 'equalp)))
12 | (apply #'format-translation
13 | (cl-locale:i18n string
14 | :locale language
15 | :params args)
16 | args))
17 |
18 | (setf *translation-backend* :cl-locale)
19 |
--------------------------------------------------------------------------------
/examples/control.html:
--------------------------------------------------------------------------------
1 | {% template control (:output-whitespace nil) (x &key items) %}
2 |
3 |
4 |
5 |
6 |
7 | {% case x %}
8 | {% :one %} It is one {% end %}
9 | {% :two %} It is two {% end %}
10 | {% t %} It is neither one or two {% end%}
11 | {%end%}
12 |
13 | {% cond %}
14 | {% (stringp x) %} Argument is a string {% end %}
15 | {% (integerp x) %} Argument is an integer {% end %}
16 | {% t %} Argument is neither a string or an integer {% end %}
17 | {% end %}
18 |
19 | {% when (stringp x) %}
20 | {% let ((result (string-upcase x))) %}
21 | {{result}}
22 | {%end%}
23 | {%end%}
24 |
25 | {% if items %}
26 |
27 | {% dolist (item items) %}
28 | - {{ item }}
29 | {% end %}
30 |
31 | {% else %}
32 | There are no items
33 | {% end %}
34 |
35 |
36 | {% end %}
37 |
--------------------------------------------------------------------------------
/examples/inheritance.html:
--------------------------------------------------------------------------------
1 | {% template parent () () %}
2 |
3 |
4 | Inheritance
5 |
6 |
7 | {% section body %}
8 | This is parent body
9 | {% end %}
10 | {% section empty %}{% end %}
11 |
12 |
13 | {% end %}
14 |
15 | {% ten:template child1 (:extends parent) () %}
16 |
17 | {% section body %}
18 | This is child1 body
19 | {% end %}
20 |
21 | {% end %}
22 |
23 | {% ten:template child2 (:extends parent) () %}
24 |
25 | {% section body %}
26 | This is child2 body
27 | {{super}}
28 | {% end %}
29 |
30 | {% end %}
31 |
32 | {% ten:template child3 (:extends child2) () %}
33 |
34 | {% section body %}
35 | {{super}}
36 | This is child3 body
37 | {% end %}
38 |
39 | {% end %}
40 |
41 | {% ten:template child4 (:extends child1) () %}
42 |
43 | {% section body %}
44 | {{super}}
45 | This is child4 body
46 | {% end %}
47 |
48 | {% end %}
49 |
50 | {% template super-ex (:extends parent) () %}
51 |
52 | {% section body %}
53 | {{ super }}
54 | This is child body
55 | {% end %}
56 |
57 | {% end %}
58 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Mariano Montone
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/i18n.lisp:
--------------------------------------------------------------------------------
1 | (in-package :ten/template)
2 |
3 | (defvar *default-language* :en)
4 | (defvar *current-language* nil)
5 |
6 | (defvar *translation-backend* nil "The translation backend.")
7 |
8 | (defvar *warn-on-untranslated-messages* t)
9 | (defvar *untranslated-messages* nil)
10 |
11 | (defun translate (string &optional args
12 | (language (or *current-language* *default-language*))
13 | (backend *translation-backend*))
14 | (apply #'backend-translate backend string language args))
15 |
16 | (defun _ (string &rest args)
17 | (apply #'translate string args))
18 |
19 | (defun format-translation (string &rest args)
20 | (apply #'format nil
21 | (ppcre:regex-replace-all
22 | "\\:(\\w*)"
23 | string
24 | (lambda (_ varname)
25 | (declare (ignore _))
26 | (let ((val (access:access args (make-keyword varname))))
27 | (or (and val (princ-to-string val))
28 | (error "~A missing in ~A translation" varname string))))
29 | :simple-calls t)
30 | args))
31 |
32 | (defgeneric backend-translate (backend string language &rest args)
33 | (:method ((backend null) string language &rest args)
34 | (error "Translation backend has not been setup"))
35 | (:method ((backend t) string language &rest args)
36 | (error "Invalid translation backend: ~A" backend)))
37 |
--------------------------------------------------------------------------------
/ten.examples.asd:
--------------------------------------------------------------------------------
1 | (asdf:defsystem #:ten.examples
2 | :description "Examples for TEN Common Lisp Template System"
3 | :author "Mariano Montone "
4 | :license "MIT"
5 | :version "0.0.1"
6 | :serial t
7 | :defsystem-depends-on (:ten)
8 | :depends-on (:ten)
9 | :components
10 | ((:module "examples"
11 | :components
12 | ((:file "package")
13 | (:ten-template "ex1" :file-extension "html" :package :ten/examples)
14 | (:ten-template "comments" :file-extension "html" :package :ten/examples)
15 | (:ten-template "inheritance" :file-extension "html" :package :ten/examples)
16 | (:ten-template "include" :file-extension "html" :package :ten/examples)
17 | (:ten-template "dot-syntax" :file-extension "html" :package :ten/examples)
18 | (:ten-template "dot-syntax-2" :file-extension "html" :package :ten/examples)
19 | (:ten-template "i18n" :file-extension "html" :package :ten/examples)
20 | (:ten-template "many" :file-extension "html" :package :ten/examples)
21 | (:ten-template "filters" :file-extension "html" :package :ten/examples)
22 | (:ten-template "escaping" :file-extension "html" :package :ten/examples)
23 | (:ten-template "control" :file-extension "html" :package :ten/examples)
24 | (:ten-template "with-output-whitespace" :file-extension "html" :package :ten/examples)
25 | ))))
26 |
--------------------------------------------------------------------------------
/asdf.lisp:
--------------------------------------------------------------------------------
1 | ;;;; This is largely structured around the CFFI Groveller:
2 | ;;;;
3 | ;;;; https://github.com/cffi/cffi/blob/master/grovel/asdf.lisp
4 | ;;;;
5 |
6 | (in-package :asdf)
7 |
8 | (defclass ten-template (source-file)
9 | ((type :initform "ten"
10 | :initarg :file-extension)
11 | (package :initform :ten-templates
12 | :initarg :package
13 | :reader template-package)))
14 |
15 | (defmethod compiled-template-path ((component ten-template))
16 | (make-pathname :type "lisp"
17 | :defaults (component-pathname component)))
18 |
19 | (defmethod output-files (op (component ten-template))
20 | nil)
21 |
22 | (defmethod perform ((op compile-op) (component ten-template))
23 | (let ((compiled-template-path (compiled-template-path component)))
24 | (with-open-file (stream compiled-template-path
25 | :direction :output
26 | :if-exists :supersede
27 | :if-does-not-exist :create)
28 | (let* ((parsed (ten/parser:parse-template
29 | (component-pathname component)))
30 | (compiled (ten/compiler:compile-template
31 | parsed
32 | (template-package component)))
33 | ;; Need to maintain reference EQuality for uninterned symbols.
34 | (*print-circle* t))
35 | (if (atom (first compiled))
36 | (print compiled stream)
37 | (mapcar (lambda (code)
38 | (print code stream))
39 | compiled))))))
40 |
41 | (defmethod perform ((op load-op) (component ten-template))
42 | (let ((compiled-template-path (compiled-template-path component)))
43 | (perform 'load-source-op
44 | (make-instance 'cl-source-file
45 | :name (component-name component)
46 | :parent (component-parent component)
47 | :pathname compiled-template-path))))
48 |
49 | (import 'ten-test :asdf)
50 |
--------------------------------------------------------------------------------
/package.lisp:
--------------------------------------------------------------------------------
1 | (defpackage ten/parser
2 | (:use :cl :esrap)
3 | (:import-from :split-sequence
4 | :split-sequence-if)
5 | (:export :
6 | :
7 | :
8 | :code
9 | :body
10 | :parse-template))
11 |
12 | (defpackage ten/template
13 | (:use :cl)
14 | (:export :template
15 | :esc
16 | :raw
17 | :verb
18 | :verbatim
19 | :begin-raw
20 | :begin-verbatim
21 | :begin-verb
22 | :super
23 | :section
24 | :comment
25 | :with-output-whitespace
26 | :_
27 | :%ten-stream
28 | :*template-output*
29 | :*escape-html*
30 | :*dot-syntax*
31 | :*create-string-writing-functions*
32 | :*create-stream-writing-functions*))
33 |
34 | (defpackage ten/compiler
35 | (:use :cl :ten/parser :ten/template)
36 | (:import-from :split-sequence
37 | :split-sequence-if)
38 | (:import-from :ten/template
39 | :%ten-stream)
40 | (:export :compile-template
41 | :*template-package*))
42 |
43 | (defpackage #:ten
44 | (:use #:cl)
45 | (:import-from :ten/template
46 | :template
47 | :esc
48 | :raw
49 | :verb
50 | :verbatim
51 | :comment
52 | :super
53 | :with-output-whitespace
54 | :section
55 | :begin-raw
56 | :begin-verb
57 | :begin-verbatim
58 | :_)
59 | (:export :expand-template
60 | :compile-template
61 | :template
62 | :esc
63 | :raw
64 | :verb
65 | :verbatim
66 | :comment
67 | :super
68 | :section
69 | :with-output-whitespace
70 | :begin-raw
71 | :begin-verb
72 | :begin-verbatim
73 | :_))
74 |
75 | (defpackage #:ten-templates
76 | (:use :cl :ten/template)
77 | (:export :super))
78 |
--------------------------------------------------------------------------------
/ten-mode.el:
--------------------------------------------------------------------------------
1 | ;;; ten-mode.el --- Minor mode for compiling TEN Lisp templates -*- lexical-binding: t; -*-
2 |
3 | ;; Copyright (C) 2022 Mariano Montone
4 |
5 | ;; Author: Mariano Montone
6 | ;; Keywords: lisp
7 |
8 | ;; This program is free software; you can redistribute it and/or modify
9 | ;; it under the terms of the GNU General Public License as published by
10 | ;; the Free Software Foundation, either version 3 of the License, or
11 | ;; (at your option) any later version.
12 |
13 | ;; This program is distributed in the hope that it will be useful,
14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | ;; GNU General Public License for more details.
17 |
18 | ;; You should have received a copy of the GNU General Public License
19 | ;; along with this program. If not, see .
20 |
21 | ;;; Commentary:
22 |
23 | ;; Minor mode for compiling TEN Lisp templates
24 | ;;
25 | ;; install like:
26 | ;; (add-hook 'web-mode-hook 'ten-mode)
27 |
28 | ;;; Code:
29 |
30 | (require 'slime)
31 |
32 | (defun ten-compile-template ()
33 | "Compile the TEN template in `current-buffer'."
34 | (interactive)
35 | (slime-eval `(ten:compile-template (cl::pathname ,(buffer-file-name))))
36 | (message "TEN template compiled."))
37 |
38 | (defun ten-expand-template ()
39 | "Expand the TEN template in `current-buffer'."
40 | (interactive)
41 | (let ((expanded (slime-eval
42 | `(cl:with-output-to-string
43 | (s)
44 | (cl:pprint (ten:expand-template (cl::pathname ,(buffer-file-name))) s)))))
45 | (slime-with-popup-buffer ("*TEN expansion*"
46 | :package :ten-templates :connection t
47 | :mode 'lisp-mode)
48 | (slime-mode 1)
49 | (slime-macroexpansion-minor-mode 1)
50 | (setq font-lock-keywords-case-fold-search t)
51 | (current-buffer)
52 | (insert expanded))))
53 |
54 | (define-minor-mode ten-mode
55 | "Minor mode for compiling and expanding TEN templates."
56 | :init-value nil
57 | :lighter " TEN"
58 | :keymap
59 | `((,(kbd "C-c C-c") . ten-compile-template)
60 | (,(kbd "C-c RET") . ten-expand-template))
61 | :group 'ten)
62 |
63 | (provide 'ten-mode)
64 | ;;; ten-mode.el ends here
65 |
--------------------------------------------------------------------------------
/tests.lisp:
--------------------------------------------------------------------------------
1 | (in-package :cl-user)
2 |
3 | (defpackage :ten/tests
4 | (:use :cl :ten :fiveam)
5 | (:export :run-tests))
6 |
7 | (in-package #:ten/tests)
8 |
9 | (def-suite ten-tests)
10 |
11 | (defun run-tests ()
12 | (debug! 'ten-tests))
13 |
14 | (in-suite ten-tests)
15 |
16 | (def-test inheritance-test ()
17 | (is (not (null (search "This is parent body" (ten/examples:parent)))))
18 | (is (not (null (search "This is child1 body" (ten/examples:child1)))))
19 | (is (null (search "This is parent body" (ten/examples:child1))))
20 | (is (not (null (search "This is child2 body" (ten/examples:child2)))))
21 | (is (not (null (search "This is parent body" (ten/examples:child2)))))
22 | (is (not (null (search "This is child3 body" (ten/examples:child3)))))
23 | (is (not (null (search "This is child2 body" (ten/examples:child3)))))
24 | (is (not (null (search "This is parent body" (ten/examples:child3))))))
25 |
26 | (def-test escaping-test ()
27 | (is (null (search "" (ten/examples:escaping2 ""))))
28 | (is (not (null (search "<div></div>" (ten/examples:escaping2 "")))))
29 | (is (not (null (search "" (ten/examples:escaping3 "")))))
30 | (is (not (null (search "" (ten/examples:escaping4 ""))))))
31 |
32 | (def-test if-expression-test ()
33 | ;; {% if %} without {% else %} should fail to compile
34 | (signals error
35 | (ten:compile-template "{% template if-test-1 () (locale) %}
36 |
37 | {% end %}"))
38 | ;; {% if %} with an {% else %} compiles
39 | (finishes
40 | (ten:compile-template "{% template if-test-2 () (locale) %}
41 |
42 | {% end %}"))
43 | ;; {% when %} does not need an else
44 | (finishes
45 | (ten:compile-template "{% template if-test-3 () (locale) %}
46 |
47 | {% end %}")))
48 |
49 | (def-test with-output-whitespace-test ()
50 | (is (string= (ten/examples:with-output-whitespace-test-1 '((:x . 1) (:y . 2)))
51 | "X1Y2"))
52 | (is (string= (ten/examples:with-output-whitespace-test-2 '((:x . 1) (:y . 2)))
53 | "X 1
54 | Y 2
55 | ")
56 | (is (string= (ten/examples:with-output-whitespace-test-3 '((:x . 1) (:y . 2)))
57 | "X 1
58 | Y 2
59 | X1Y2"))
60 | ))
61 |
--------------------------------------------------------------------------------
/parser.lisp:
--------------------------------------------------------------------------------
1 | (in-package :ten/parser)
2 |
3 | ;;; Utilities
4 |
5 | (defparameter +whitespace+
6 | (list #\Space #\Tab #\Newline #\Linefeed #\Backspace
7 | #\Page #\Return #\Rubout))
8 |
9 | (defun whitespacep (char)
10 | (member char +whitespace+))
11 |
12 | (defun trim-whitespace (str)
13 | (string-trim +whitespace+ str))
14 |
15 | ;;; Element classes
16 |
17 | (defclass () ())
18 |
19 | (defclass ()
20 | ((code :reader code :initarg :code)))
21 |
22 | (defclass ()
23 | ())
24 |
25 | (defclass ()
26 | ())
27 |
28 | (defclass ()
29 | ((code :reader code :initarg :code)
30 | (body :reader body :initarg :body :initform nil)))
31 |
32 | (defclass ()
33 | ())
34 |
35 | (defclass () ())
36 |
37 | (defmethod print-object ((tag ) stream)
38 | (format stream "<~a ~a>"
39 | (class-name (class-of tag))
40 | (code tag)))
41 |
42 | (defmethod print-object ((tag ) stream)
43 | (format stream "<~a ~a ~a>"
44 | (class-name (class-of tag))
45 | (code tag)
46 | (body tag)))
47 |
48 | ;;; Parsing rules
49 |
50 | (defparameter +start-output-delimiter+ "{{")
51 | (defparameter +end-output-delimiter+ "}}")
52 | (defparameter +start-control-delimiter+ "{%")
53 | (defparameter +end-control-delimiter+ "%}")
54 |
55 | (defparameter +start-comment-delimiter "{#")
56 | (defparameter +stop-comment-delimiter "#}")
57 |
58 | (defrule comment (and "{#" (+ (not "#}")) "#}")
59 | (:lambda (list)
60 | (declare (ignore list))
61 | ""))
62 |
63 | (defrule control-string (+ (not "%}"))
64 | (:text t))
65 |
66 | (defrule control-tag (and "{%";;+start-control-delimiter+
67 | control-string
68 | "%}";;+end-control-delimiter+
69 | )
70 | (:destructure (open code close)
71 | (declare (ignore open close))
72 | (let ((text (trim-whitespace code)))
73 | (cond
74 | ((equal text "end")
75 | (make-instance '))
76 | ((equal text "else")
77 | (make-instance '))
78 | (t (make-instance ' :code text))))))
79 |
80 | (defrule output-string (+ (not "}}"))
81 | (:lambda (list) (text list)))
82 |
83 | (defrule output-tag (and "{{";;+start-output-delimiter+
84 | output-string
85 | "}}";;+end-output-delimiter+
86 | )
87 | (:destructure (open code close)
88 | (declare (ignore open close))
89 | (let ((text (trim-whitespace code)))
90 | (if (find #\space text)
91 | (make-instance ' :code text)
92 | (make-instance ' :code text)))))
93 |
94 | (defrule raw-text (+ (not (or "{{" ;;+start-output-delimiter+)
95 | "{%" ;;+start-control-delimiter+)
96 | "{#") ;;+start-comment-delimiter+)
97 | ))
98 | (:lambda (list) (text list)))
99 |
100 | (defrule expr (+ (or comment control-tag output-tag raw-text)))
101 |
102 | (defun tokenize-template (string)
103 | (parse 'expr string))
104 |
105 | (defun def-control-without-body (symbol)
106 | (setf (getf (symbol-plist symbol)
107 | :ten-control-without-body)
108 | t))
109 |
110 | ;;; Token parsing
111 | ;;; Take a list of either strings or s and turn it into a tree
112 |
113 | (defun parse-tokens (tokens)
114 | (let ((tokens (copy-list tokens)))
115 | (labels ((next-token ()
116 | (prog1 (first tokens)
117 | (setf tokens (rest tokens))))
118 | (rec-parse (&optional toplevel)
119 | (let ((out (make-array 1 :adjustable 1 :fill-pointer 0))
120 | (tok (next-token)))
121 | (loop while (and tok (not (typep tok '))) do
122 | (vector-push-extend
123 | (cond
124 | ((typep tok ')
125 | ;; Start a block
126 | (make-instance (class-of tok)
127 | :code (code tok)
128 | :body (rec-parse)))
129 | (t tok))
130 | out)
131 | (setf tok (next-token))
132 | (when (and (not tok) (not toplevel)) ;; Next tok is nil
133 | (error "Missing 'end' tag" )))
134 | out)))
135 | (rec-parse t))))
136 |
137 | (defun parse-template (string-or-pathname)
138 | (if (pathnamep string-or-pathname)
139 | (parse-template (alexandria:read-file-into-string string-or-pathname))
140 | (parse-tokens (tokenize-template string-or-pathname))))
141 |
--------------------------------------------------------------------------------
/template.lisp:
--------------------------------------------------------------------------------
1 | (in-package :ten/template)
2 |
3 | (defvar *escape-html* t)
4 | (defvar *dot-syntax* t)
5 | (defvar *output-whitespace* t)
6 | (defvar *export-template* t "Export the templates by default")
7 |
8 | (defvar *template-output* nil
9 | "The stream stream writing template functions write to")
10 | (defvar *compiling-template*)
11 | (defvar *create-string-writing-functions* t)
12 | (defvar *create-stream-writing-functions* nil)
13 |
14 | (defclass template ()
15 | ())
16 |
17 | (defun string-writing-function-name (name)
18 | name)
19 |
20 | (defun stream-writing-function-name (name)
21 | (intern (format nil "~a*" name)
22 | (symbol-package name)))
23 |
24 | (defgeneric render-template (template stream))
25 |
26 | (defun esc (string)
27 | "Escape a string."
28 | (if *escape-html*
29 | (who:escape-string string)
30 | string))
31 |
32 | (defun lambda-list-slots (args)
33 | (multiple-value-bind (required optional rest keyword)
34 | (alexandria:parse-ordinary-lambda-list args)
35 | (let* ((slots (append (mapcar (lambda (r)
36 | (list r :initarg (intern (symbol-name r) :keyword))) required)
37 | (mapcar (lambda (o)
38 | (list (first o)
39 | :initarg (intern (symbol-name (first o)) :keyword)
40 | :initform (second o)))
41 | optional)
42 | (when rest
43 | (list rest :initarg (intern (symbol-name rest) :keyword)))
44 | (mapcar (lambda (k)
45 | (list (second (first k))
46 | :initarg (intern (symbol-name (second (first k))) :keyword)
47 | :initform (second k)))
48 | keyword)))
49 | (arg-names (append required
50 | (mapcar 'first optional)
51 | (when rest
52 | (list rest))
53 | (mapcar (alexandria:compose 'second 'first)
54 | keyword)))
55 | (slots-init (loop
56 | for arg in arg-names
57 | appending (list (intern (symbol-name arg) :keyword) arg))))
58 | (values slots slots-init arg-names))))
59 |
60 | (defmacro template (name (&key extends
61 | package
62 | (escape-html *escape-html*)
63 | (export *export-template*)
64 | (dot-syntax *dot-syntax*)
65 | (output-whitespace *output-whitespace*)
66 | (create-string-writing-function *create-string-writing-functions*)
67 | (create-stream-writing-function *create-stream-writing-functions*))
68 | args &rest body)
69 |
70 | "The main template creation macro.
71 |
72 | Arguments:
73 |
74 | - EXTENDS: the name of the template to extend from.
75 | - PACKAGE: the package within which the compiled template is to be defined.
76 | - EXPORT: when T, the generated template function is exported.
77 | - ESCAPE-HTML: whether to escape HTML or not. Default is controlled by *ESCAPE-HTML*, which is true by default.
78 | - OUTPUT-WHITESPACE: whether to output whitespaces or not. Default is controlled by *OUTPUT-WHITESPACE*, which is true by default.
79 | - CREATE-STRING-WRITING-FUNCTION: controls whether a function that writes the template to a string should be created. Default is T.
80 | - CREATE-STREAM-WRITING-FUNCTION: controls whether a function that writes the template to *TEMPLATE-OUTPUT* stream should be created. Default is false.
81 |
82 | IMPORTANT: some of this macro arguments are processed by CALL-WITH-TEMPLATE-HEADER-OPTIONS, not here. That's why we declare some of them as ignored."
83 |
84 | (declare (ignore package output-whitespace escape-html))
85 |
86 | (multiple-value-bind (slots slots-init arg-names)
87 | (lambda-list-slots args)
88 | (let ((body (if dot-syntax
89 | `((access:with-dot ()
90 | ,@body))
91 | body)))
92 | `(progn
93 |
94 | (defclass ,name (,(or extends 'template))
95 | ,slots)
96 |
97 | ,@(when (not extends)
98 | `((defmethod render-template ((%ten-template ,name) %ten-stream)
99 | (with-slots ,arg-names %ten-template
100 | ,@body))))
101 |
102 | ,@(when create-string-writing-function
103 | (let ((fname (string-writing-function-name name)))
104 | `((defun ,fname ,args
105 | (let ((%ten-template (make-instance ',name ,@slots-init)))
106 | (values
107 | (with-output-to-string (%ten-stream)
108 | (render-template %ten-template %ten-stream))
109 | t)))
110 | (compile ',fname)
111 | ,@(when export
112 | `((export ',fname (symbol-package ',name)))))))
113 |
114 | ,@(when create-stream-writing-function
115 | (let ((fname (stream-writing-function-name name)))
116 | `((defun ,fname ,args
117 | (let ((%ten-template (make-instance ',name ,@slots-init)))
118 | (values
119 | (render-template %ten-template *template-output*))
120 | t))
121 | (compile ',fname)
122 | ,@(when export
123 | `((export ',fname (symbol-package ',name)))))))
124 | ))))
125 |
126 | (defmacro begin-raw (&body body)
127 | `(let ((*escape-html* nil))
128 | ,@body))
129 |
130 | (defmacro begin-verb (&body body)
131 | `(flet ((esc (string)
132 | string))
133 | ,@body))
134 |
135 | (defmacro begin-verbatim (&body body)
136 | `(flet ((esc (string)
137 | string))
138 | ,@body))
139 |
140 | (defmacro comment (&body body)
141 | (declare (ignore body))
142 | "")
143 |
144 | (defun raw (str)
145 | (values str t))
146 |
147 | (defun verb (str)
148 | (values str t))
149 |
150 | (defun verbatim (str)
151 | (values str t))
152 |
153 | (define-symbol-macro super
154 | (progn
155 | (call-next-method)
156 | ""))
157 |
158 | (defmacro with-output-whitespace (value &body body)
159 | `(let ((*output-whitespace* ,value))
160 | ,@body))
161 |
--------------------------------------------------------------------------------
/compiler.lisp:
--------------------------------------------------------------------------------
1 | (in-package :ten/compiler)
2 |
3 | (defparameter *template-package*
4 | (find-package 'ten-templates)
5 | "The package where template expressions are evaluated and the template function is exported")
6 |
7 | (defvar *compiling-template*) ;; The template being compiled
8 | (defvar *sections*) ;; Sections of current template
9 | (defvar *output-whitespace*)
10 |
11 | (defgeneric render-section (section-name template stream))
12 |
13 | (defun read-template-expressions (string)
14 | (let ((*package* *template-package*)
15 | (end '#:eof))
16 | (with-input-from-string (s string)
17 | (loop
18 | for form = (read s nil end)
19 | until (eq form end)
20 | collect form))))
21 |
22 | (defun extract-filters (string)
23 | (let ((parts (mapcar 'read-template-expressions (split-sequence:split-sequence #\| string))))
24 | (values (first parts) (rest parts))))
25 |
26 | (defun apply-filters (code filters)
27 | (loop
28 | :with result := code
29 | :for filter in filters
30 | :do (setf result
31 | (alexandria:if-let
32 | ((pos (position '_ filter :test 'equalp)))
33 | (let ((replaced filter))
34 | (setf (nth pos replaced) result)
35 | replaced)
36 | (list* (first filter)
37 | result
38 | (rest filter))))
39 | :finally (return result)))
40 |
41 | ;;; Compiler
42 |
43 | (defmethod emit ((str string))
44 | (when (not (and (not *output-whitespace*)
45 | (every 'ten/parser::whitespacep str)))
46 | `(write-string ,str %ten-stream)))
47 |
48 | (defmethod emit ((vec vector))
49 | `(progn ,@(loop for elem across vec collecting (emit elem))))
50 |
51 | (defmethod emit ((tag ))
52 | (multiple-value-bind (expr filters)
53 | (extract-filters (code tag))
54 | (alexandria:with-unique-names (out raw)
55 | `(multiple-value-bind (,out ,raw)
56 | ,(apply-filters
57 | (if (= (length expr) 1)
58 | (first expr)
59 | expr)
60 | filters)
61 | (if ,raw
62 | ;; if the second return value is T, the result of the expression
63 | ;; is not escaped
64 | (princ ,out %ten-stream)
65 | (write-string (esc (princ-to-string ,out))
66 | %ten-stream))))))
67 |
68 | (defun else-tag-p (element)
69 | (typep element '))
70 |
71 | (defmethod emit ((tag ))
72 | (flet ((emit-body (body)
73 | (let ((else-tag-pos (position-if 'else-tag-p body)))
74 | (loop
75 | for elem in (if else-tag-pos
76 | (split-sequence-if 'else-tag-p body)
77 | (coerce body 'list))
78 | for output := (emit elem)
79 | when output
80 | collect output))))
81 | (let ((exprs (read-template-expressions (code tag))))
82 | (case (first exprs)
83 | (ten/template:with-output-whitespace ;; control whitespace output
84 | (let ((*output-whitespace* (cadr exprs)))
85 | `(,@exprs ,@(emit-body (body tag)))))
86 | (ten/template::section ;; sections are a special case
87 | ;; push the section to the list of sections
88 | ;; to generate render-section methods later
89 | (push (list (second exprs)
90 | (emit-body (body tag)))
91 | *sections*)
92 | `(ten/compiler::render-section ',(second exprs) ten/template::%ten-template %ten-stream))
93 | (cl:if ;; check there's an else tag in body and emit
94 | (when (not (find-if (lambda (x) (typep x 'ten/parser::))
95 | (body tag)))
96 | (error "Missing {% else %} in {% if %} expression: ~a"
97 | tag))
98 | `(,@exprs ,@(emit-body (body tag))))
99 | (t ;; otherwise, just emit
100 | `(,@exprs ,@(emit-body (body tag))))))))
101 |
102 | (defun control-element-p (element)
103 | (typep element '))
104 |
105 | (defun compile-template (elements &optional (package-name 'ten/template))
106 | (loop
107 | for element across elements
108 | when (not (stringp element))
109 | appending
110 | (let ((*template-package* (find-package package-name)))
111 | (call-with-template-header-options
112 | element
113 | (lambda () (emit element))))))
114 |
115 | (defun start-template-compilation (template-name)
116 | (declare (ignore template-name)))
117 |
118 | (defun finish-template-compilation (template-name result)
119 | (declare (ignore template-name))
120 | ;; Handle the sections here
121 | (append result
122 | (loop
123 | for section in *sections*
124 | collect
125 | (destructuring-bind (section-name body) section
126 | (multiple-value-bind (slots slots-init arg-names)
127 | (ten/template::lambda-list-slots (getf *compiling-template* :args))
128 | (declare (ignore slots slots-init))
129 | (let ((body (if (if (not (member :dot-syntax (getf *compiling-template* :options)))
130 | ten/template::*dot-syntax*
131 | (getf (getf *compiling-template* :options) :dot-syntax))
132 | `((access:with-dot ()
133 | ,@body))
134 | body)))
135 | `(defmethod render-section ((section (eql ',section-name))
136 | (ten/template::%ten-template ,(getf *compiling-template* :name))
137 | %ten-stream)
138 | (declare (ignore section))
139 | (declare (ignorable %ten-stream))
140 | (with-slots ,arg-names ten/template::%ten-template
141 | ,@body))))))))
142 |
143 | (defun call-with-template-header-options (header func)
144 | (let ((expr (read-template-expressions (code header))))
145 | (if (eql (first expr) 'ten/template:template)
146 | (let ((*template-package* (let ((package-name (getf (third expr) :package)))
147 | (if package-name
148 | (or (find-package package-name)
149 | (error "Package not found: ~s" package-name))
150 | *template-package*))))
151 | (destructuring-bind (_ template-name options args)
152 | (read-template-expressions (code header)) ;; read the header again, in correct package
153 | (declare (ignore _))
154 | (let* ((*compiling-template* (list
155 | :name template-name
156 | :options options
157 | :args args))
158 | (*sections* nil)
159 | (*output-whitespace* (if (member :output-whitespace options)
160 | (getf options :output-whitespace)
161 | ten/template::*output-whitespace*)))
162 | (start-template-compilation template-name)
163 | (let ((compiled-template (funcall func)))
164 | (finish-template-compilation template-name (list compiled-template))))))
165 | (funcall func))))
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TEN
2 |
3 | [](https://travis-ci.org/mmontone/ten)
4 | [](http://quickdocs.org/ten/)
5 | [](./LICENSE)
6 |
7 | Yet another template system for Common Lisp.
8 |
9 | TEN is a fork of [ECO template system](https://github.com/eudoxia0/eco) by Fernando Borretti.
10 |
11 | Like ECO, TEN compiles templates to Lisp code, but has some differences:
12 | - Two types of tags only. Control and output.
13 | - Support for templates inheritance.
14 | - Dot syntax for accessing template data.
15 | - Convenient syntax for applying filters.
16 | - Configurable syntax delimiters (planned, not done yet).
17 |
18 | My reasons for writing yet another template system for Common Lisp is to combine the simplicity and usability of ECO (the whole Lisp language at your disposal for writing the templates), with features available in more complex template systems like [Djula](https://mmontone.github.io/djula/) that makes things easier for web development (inheritance, dot syntax, etc.).
19 |
20 | ## Usage
21 |
22 | A TEN template looks like this:
23 |
24 | ```jinja
25 | {% template ex1 () (user enabled &key items) %}
26 |
27 |
28 |
29 |
30 |
31 | {{ user.name | string-capitalize }}
32 |
33 | {% if enabled %}
34 | Enabled
35 | {% else %}
36 | Disabled
37 | {% end %}
38 |
39 | {% when items %}
40 |
41 | {% loop for item in items do %}
42 | - {{ item }}
43 | {% end %}
44 |
45 | {% end %}
46 |
47 | {% when (not items) %}
48 | There are no items
49 | {% end %}
50 |
51 |
52 | {% end %}
53 |
54 | ```
55 |
56 | These are the types of tags:
57 | - *Output tags*: `{{ }}`, becomes ``, and `{{ &rest args }}`, that becomes `(fn arg1 arg2 .. argn)`.
58 | - *Control tags*: `{% %} body {% end %}`, becomes `( body)`.
59 | - *Comments tags*: Use `{#` and `#}` to comment out a piece of template.
60 |
61 | Control tags control which parts of the tamplate are rendered; their return value is ignored.
62 |
63 | The value returned by output tags are interpolated into the template. The function called can be any
64 | Lisp function, or another template (because templates are compiled to functions).
65 |
66 | For example:
67 |
68 | * `{{ user }}` => `user`
69 | * `{{ name user }}` => `(name user)`
70 | * `{% when (name user) %} ... {% end %}` => `(when (name user) ...)`
71 |
72 | The `if` tag is a special case: it supports using an `else` tag to separate the true and
73 | false branches. For example:
74 |
75 | ```lisp
76 | {% if posts %}
77 | Recent Posts
78 | ... loop over posts ...
79 | {% else %}
80 | No recent posts.
81 | {% end %}
82 | ```
83 |
84 | Also, more [advanced control expressions](https://github.com/mmontone/ten/blob/master/examples/control.html) are possible, like `let`, `case`, `cond`, etc.
85 |
86 | ## Template definition
87 |
88 | Templates are defined with the following syntax:
89 |
90 | ```
91 | {% template name (&rest options) (&rest args) %}
92 | ... body ...
93 | {% end %}
94 | ```
95 |
96 | Template options are:
97 | - `:extends` : The template to extend from.
98 | - `:dot-syntax`: If T, templates are compiled with dot syntax enabled. Dot syntax is implemented via the Lisp library `access`. Default is T.
99 | - `:package`: The package in which to compile and export the template. By default, templates are compiled and exported in `TEN-TEMPLATES` package.
100 | - `:export`: When T, export the generated template function. Otherwise, the template is not exported. Default is T.
101 | - `:escape-html`: Whether to escape html in output tags. Default is T.
102 | - `:output-whitespace`. Default is T. When NIL, expressions that just spit whitespace are discarded.
103 |
104 | ## Template compilation
105 |
106 | For manually compiling templates, use `ten:compile-template` function.
107 |
108 | But more useful is to include them in the ASDF system definition of your project.
109 |
110 | First, add `:ten` as ASDF system definition dependency:
111 |
112 | `:defsystem-depends-on (:ten)`
113 |
114 | Then, use `:ten-template` in to include the template files:
115 |
116 | ```lisp
117 | (:ten-template "filename")
118 | ```
119 |
120 | The default file extension is "ten", but another can be specified via the `:file-extension` option; and the template package can be specified with the `:package` option. Look at [ten.examples ASDF system](https://github.com/mmontone/ten/blob/master/ten.examples.asd) for an example.
121 |
122 | You can also compile all the templates in some directory using this ASDF recipe:
123 |
124 | ```lisp
125 | :perform (asdf:compile-op :after (o c)
126 | (dolist (template (uiop:directory-files (asdf:system-relative-pathname :my-app "templates/*.ten")))
127 | (uiop:symbol-call :ten 'compile-template template)))
128 | ```
129 |
130 | Templates are compiled into functions and exported in the indicated package. The default package is `ten-templates`, but that can be changed from either the ASDF system definition, the `ten:compile-template` parameters, or the `{% template %}` options.
131 |
132 | If `CREATE-STREAM-WRITING-FUNCTION` option is enabled, then two functions are compiled, the default one, that compiles to a function that takes template arguments and renders the template to a string. And a function that renders the template to the `*TEMPLATE-OUTPUT*` stream. So, for a template named `my-template`, a `my-template` function that renders to a string is created, and a `my-template*` function that renders to `*TEMPLATE-OUTPUT*` stream is created.
133 |
134 | When developing your project it is useful to be able to compile templates in an interactive way.
135 | If you are using Emacs + SLIME, load `ten.el` file.
136 |
137 | Then use `M-X ten-compile-template` when on the template buffer to compile templates. Note that you may want to have `:package` option specified in the template so that it gets compiled into the correct package.
138 |
139 | For debugging, you can inspect the expanded template using `ten:expand-template` function. In Emacs, go to template buffer an do `M-x ten-expand-template`.
140 |
141 | If you enable `ten` minor mode, template compilation gets conveniently bound to `C-c C-c`, and template expansion to `C-c RET`. Best is to automatically enable the minor mode for template files adding something like `(add-hook 'web-mode-hook 'ten-mode)` to your `.emacs` initialization file.
142 |
143 | ## Inheritance
144 |
145 | To make a template inherit from anohter, use the `:extends` option in template definition.
146 |
147 | Templates are organized in `sections`. `sections` are the parts of the templates that are inherited.
148 |
149 | Use `{{super}}` inside a `section` to render the parent `section`.
150 |
151 | TEN leverages CLOS for implementing template inheritance. Templates are compiled to classes and generic functions `render-template` and `render-section`.
152 |
153 | Have a look at some [examples of template inheritance](https://github.com/mmontone/ten/blob/master/examples/inheritance.html).
154 |
155 | ## Includes
156 |
157 | To include other templates, just use the output tag with the name of the included template. Remember that templates are compiled to functions; just call those functions from the template to include them.
158 |
159 | Have a look at [an example](https://github.com/mmontone/ten/blob/master/examples/include.html).
160 |
161 | ## Dot syntax
162 |
163 | When dot syntax is enabled (it is, by default), it is possible to conveniently access objects with dot syntax in templates:
164 |
165 | `{{ object.key1.key2 }}`
166 |
167 | that gets translated by [access](https://github.com/AccelerationNet/access) library to:
168 |
169 | `(access:accesses obj 'key1 'key2)`
170 |
171 | Have a look at [an example](https://github.com/mmontone/ten/blob/master/examples/dot-syntax.html).
172 |
173 | ## Filters
174 |
175 | TEN implements some convenient syntax for filters.
176 |
177 | `{{ value | func1 arg1 .. argN| func2 arg1 .. argN| .. | funcN arg1 .. argN}}`
178 |
179 | Filters are just normal functions that get applied to the value.
180 |
181 | Filters are translated to functions application like this:
182 |
183 | `(funcN (.. (func2 (func1 value arg1 .. argN) arg1 .. argN))) arg1 .. argN)`
184 |
185 | In general, filter functions are expected to receive the value to be filtered as first parameter.
186 | But, for several Lisp functions that's not the case. In those cases, it is possible to use `_` to indicate where the filter function should receive the value.
187 |
188 | For example, `string-trim` receives the string to trim as second value, so, to apply it as filter we do:
189 |
190 | `{{str | string-trim '(#\%) _}}`
191 |
192 | Filters syntax is completly optional, you can disregard it and just apply functions instead:
193 |
194 | `{{ string-trim '(#\%) (string-capitalize str) }}`
195 |
196 | Have a look at some [examples of filters](https://github.com/mmontone/ten/tree/master/examples/filters.html).
197 |
198 | ## Examples
199 |
200 | Load and have a look at the [examples](https://github.com/mmontone/ten/tree/master/examples).
201 |
202 | ```lisp
203 | (require :ten.examples)
204 | ```
205 |
206 | Example templates get compiled and exported to `ten/examples` package.
207 |
208 | ## Troubleshooting
209 |
210 | 1) When specifying a template package other than `ten-templates`, if the package specified doesn't `:use` `ten` or `ten-template` packages, then you may run into problems trying to compile your templates. That may be because the `template` and `section` macros are not found in the specified package. In that case, make sure to prefix your `template` and `section` declarations with `ten:`, like:
211 |
212 | ```django
213 | {% ten:template my-template (:package my-package) %}
214 | {% ten:section my-section %}
215 | {% end %}
216 | {% end %}
217 | ```
218 |
219 | 2) You can use different implementation and export packages. Qualify the template name with the package name, like:
220 | ```django
221 | {% ten:template my-templates:my-template (:package my-package) %}
222 | {{ foo "bar" }}
223 | {% ten:section my-section %}
224 | {% end %}
225 | {% end %}
226 | ```
227 | That will define `MY-TEMPLATE` on the `MY-TEMPLATES` package, but use `MY-PACKAGE` in its source (the `{{ foo "bar" }}` expression expands to to `(MY-PACKAGE::FOO "bar")`). Note that you need to previously define a package `MY-TEMPLATE` that exports `MY-TEMPLATE`. Or otherwise use `MY-TEMPLATE::MY-TEMPLATE` as template name.
228 |
229 | 3) Some "complex" expressions, like `cond` and `case`, require that you turn `:output-whitespace` to `NIL`. Otherwise, template compilation puts `write-string` expressions right in the middle of the `case` and `cond` bodies. Have a look at [this template](https://github.com/mmontone/ten/blob/master/examples/control.html).
230 |
231 | ## License
232 |
233 | MIT
234 |
--------------------------------------------------------------------------------