├── .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 | 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 | [![Build Status](https://travis-ci.org/mmontone/ten.svg?branch=master)](https://travis-ci.org/mmontone/ten) 4 | [![Quicklisp](http://quickdocs.org/badge/ten.svg)](http://quickdocs.org/ten/) 5 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](./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 | --------------------------------------------------------------------------------