pax_global_header00006660000000000000000000000064125707745020014523gustar00rootroot0000000000000052 comment=684f2271f8345bba9a12d63288fc3ee638d77c09 cl-mustache/000077500000000000000000000000001257077450200132745ustar00rootroot00000000000000cl-mustache/.gitignore000066400000000000000000000000071257077450200152610ustar00rootroot00000000000000*.fasl cl-mustache/.travis.yml000066400000000000000000000002001257077450200153750ustar00rootroot00000000000000language: lisp before_install: - sudo apt-get update -qq - sudo apt-get install -qq sbcl cl-quicklisp script: ./t/run-test.sh cl-mustache/README.rst000066400000000000000000000043441257077450200147700ustar00rootroot00000000000000======== CL-MUSTACHE ======== .. image:: https://travis-ci.org/kanru/cl-mustache.png?branch=master :target: https://travis-ci.org/kanru/cl-mustache Inspired by ctemplate_ and et_, Mustache_ is a framework-agnostic way to render logic-free views. As ctemplates says, "It emphasizes separating logic from presentation: it is impossible to embed application logic in this template language." CL-MUSTACHE is a Common Lisp implementation of Mustache v1.1.2+λ. Tested with: - SBCL 1.0.55 - CLISP 2.49 CL-MUSTACHE is semantically versioned: http://semver.org. Documentation ============= The different Mustache tags are documented at `mustache(5)`_. Install It ========== Using quicklisp is recommended. :: CL-USER> (ql:quickload "cl-mustache") Use It ====== Currently accepts context data in alist format, for example: :: `((:tag . "string") (:array . #(1 2 3 4)) (:lambda ,(lambda () "world")) (:nested . ((:data . t)))) To render the template: :: CL-USER> (mustache:render* "Hi {{person}}!" '((:person . "Mom"))) "Hi Mom!" Or save the renderer for later use: :: CL-USER> (setf view (mustache:compile-template "Hi {{person}}!")) Or define static renderer function: :: CL-USER> (mustache:define view "Hi {{person}}!") CL-USER> (view context) Test It ======= :: CL-USR> (ql:quickload "cl-mustache-test") CL-USR> (mustache-test:run) Extend It (Experimental) ======================== Define your tag classes, tag character and render function: :: (in-package :mustache) (defclass exec-tag (non-standalone-tag) ((command :initarg :command :accessor command))) (set-mustache-character #\$ (lambda (raw-text arg-text escapep start end) (make-instance 'exec-tag :command arg-text))) ;; or ;; (define-mustache-character #\$ ;; (make-instance 'exec-tag :command arg-text)) (defmethod render-token ((token exec-tag) context template) (print-data (run-program-output (command token)) t context)) .. _ctemplate: http://code.google.com/p/google-ctemplate/ .. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html .. _Mustache: http://mustache.github.com/ .. _mustache(5): http://mustache.github.com/mustache.5.html cl-mustache/cl-mustache-test.asd000066400000000000000000000006471257077450200171560ustar00rootroot00000000000000(asdf:defsystem cl-mustache-test :version "2.0.0" :author "Kan-Ru Chen " :licence "MIT/Expat" :description "Test for CL-MUSTACHE" :depends-on ("cl-mustache" "prove") :defsystem-depends-on (:prove-asdf) :components ((:module "t" :components ((:test-file "test-spec") (:test-file "test-api") (:static-file "test.mustache"))))) cl-mustache/cl-mustache.asd000066400000000000000000000007601257077450200161750ustar00rootroot00000000000000(asdf:defsystem cl-mustache :version #.(with-open-file (stream (merge-pathnames "version.lisp-expr" *load-truename*)) (read stream)) :author "Kan-Ru Chen " :licence "MIT/Expat" :description "Mustache Template Renderer" :components ((:file "packages") (:file "mustache" :depends-on ("packages")) (:file "compat-api-v1" :depends-on ("mustache"))) :depends-on ("uiop")) cl-mustache/compat-api-v1.lisp000066400000000000000000000060721257077450200165500ustar00rootroot00000000000000;;;; compat-api-v1.lisp --- APIv1 compatibility package ;;; Copyright (C) 2014 Kan-Ru Chen (陳侃如) ;;; Author(s): Kan-Ru Chen (陳侃如) ;;; Permission is hereby granted, free of charge, to any person obtaining a ;;; copy of this software and associated documentation files (the "Software"), ;;; to deal in the Software without restriction, including without limitation ;;; the rights to use, copy, modify, merge, publish, distribute, sublicense, ;;; and/or sell copies of the Software, and to permit persons to whom the ;;; Software is furnished to do so, subject to the following conditions: ;;; The above copyright notice and this permission notice shall be included in ;;; all copies or substantial portions of the Software. ;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING ;;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER ;;; DEALINGS IN THE SOFTWARE. ;;;; Code: (in-package :mustache) (defvar *mustache-output* (make-synonym-stream '*output-stream*) "Deprecated in favor of MUSTACHE:*OUTPUT* since version 0.10.0") (define-condition deprecation-warning (style-warning) ((name :initarg :name :reader deprecated-name) (replacements :initarg :replacement :reader deprecated-name-replacement) (since :initarg :since :reader deprecated-since)) (:report (lambda (condition stream) (format stream "~S is deprecated since CL-MUSTACHE version ~A. ~ Use ~S instead." (deprecated-name condition) (deprecated-since condition) (deprecated-name-replacement condition))))) (defmacro make-obsolete (obsolete-name current-name when) `(defun ,obsolete-name (&rest args) ,(documentation current-name 'function) (warn 'deprecation-warning :name ',obsolete-name :replacement ',current-name :since ,when) (apply ',current-name args))) (make-obsolete mustache-type version "0.10.0") (make-obsolete mustache-version version "0.10.0") (make-obsolete mustache-context make-context "0.10.0") (make-obsolete mustache-compile compile-template "0.10.0") (make-obsolete mustache-render render "0.10.0") (make-obsolete mustache-render-to-string render* "0.10.0") (defun mustache-render-to-stream (stream template &optional context) "Render TEMPLATE with optional CONTEXT to STREAM." (warn 'deprecation-warning :name 'mustache-render-to-stream :replacement 'render :since "0.10.0") (render template context stream)) (defmacro defmustache (name template) #.(documentation 'define 'function) (warn 'deprecation-warning :name 'defmustache :replacement 'render :since "0.10.0") `(define ,name ,template)) ;;; compat-api-v1.lisp ends here ;;; Local Variables: ;;; mode: lisp ;;; End: cl-mustache/mustache.lisp000066400000000000000000000613561257077450200160110ustar00rootroot00000000000000;;;; mustache.lisp --- Mustache Template Renderer ;;; Copyright (C) 2012, 2013 Kan-Ru Chen ;;; Author: Kan-Ru Chen ;;; Permission is hereby granted, free of charge, to any person obtaining a copy of ;;; this software and associated documentation files (the "Software"), to deal in ;;; the Software without restriction, including without limitation the rights to ;;; use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies ;;; of the Software, and to permit persons to whom the Software is furnished to do ;;; so, subject to the following conditions: ;;; The above copyright notice and this permission notice shall be included in all ;;; copies or substantial portions of the Software. ;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ;;; SOFTWARE. ;;;; Commentary: ;;; Mustache is a kind of logic-less template formats. ;;; See also: http://mustache.github.com/ ;;;; TODO: ;;; ;;; * Optimize lambda sections ;;; * Optimize compiled renderer ;;; * Better error reporting ;;;; Code: (in-package :mustache) ;;; Types (deftype offset () "File or string offset." '(unsigned-byte 32)) (deftype space-char () '(member #\Space #\Tab)) (deftype newline-char () '(member #\Linefeed #\Return)) (deftype text-char () '(and character (not (or space-char newline-char)))) (defun space-char-p (char) (declare (inline)) (typep char 'space-char)) (defun newline-char-p (char) (declare (inline)) (typep char 'newline-char)) (defun text-char-p (char) (declare (inline)) (typep char 'text-char)) (defclass token () ()) (defclass beginning-of-line (token) ()) (defclass text (token) ((%text :type string :initarg :text :accessor text))) (defclass whitespace (text) ()) (defclass newline (text) ((%text :initform #.(coerce '(#\Linefeed) 'string)))) (defvar crlf (coerce '(#\Return #\Linefeed) 'string)) (defclass crlf-newline (newline) ((%text :initform crlf))) (defclass tag (token) ((%text :type string :initarg :text :accessor text) (%escapep :type boolean :initarg :escape :initform t :reader escapep) (%indent :type list :initarg :indent :initform () :accessor indent))) (defclass can-standalone-tag (tag) ()) (defclass non-standalone-tag (tag) ()) (defclass normal-tag (non-standalone-tag) ()) (defclass ampersand-tag (non-standalone-tag) ((%escapep :initform nil))) (defclass delimiter-tag (can-standalone-tag) ()) (defclass comment-tag (can-standalone-tag) ()) (defclass partial-tag (can-standalone-tag) ()) (defclass section-start-tag (can-standalone-tag) ((%falsey :type boolean :initarg :falsey :initform nil :accessor falsey) (%end :type offset :initarg :end :initform 0 :accessor end) (%open-delimiter :type string :initarg :open-delimiter :initform "" :accessor open-delimiter) (%close-delimiter :type string :initarg :close-delimiter :initform "" :accessor close-delimiter))) (defclass section-end-tag (can-standalone-tag) ((%start :type offset :initarg :start :initform 0 :accessor start))) (defclass section-tag (section-start-tag section-end-tag) ((%tokens :type list :initarg :tokens :accessor tokens))) ;;; Delimiter (defparameter *default-open-delimiter* "{{") (defparameter *default-close-delimiter* "}}") (defparameter *default-triple-open-delimiter* "{{{") (defparameter *default-triple-close-delimiter* "}}}") (defvar *open-delimiter* *default-open-delimiter*) (defvar *close-delimiter* *default-close-delimiter*) (defvar *triple-open-delimiter* *default-triple-open-delimiter*) (defvar *triple-close-delimiter* *default-triple-close-delimiter*) (defun change-delimiter (text) "Change the mustache tag delimiter according to TEXT. The syntax grammar is: delimiter-tag = left-d 1*space right-d left-d = *ALPHANUM right-d = *ALPHANUM space = #\\Space #\\Tab" (declare (type string text)) (let* ((left-edge (position #\Space text)) (right-edge (position #\Space text :from-end t))) (unless (and left-edge right-edge (every #'space-char-p (subseq text left-edge right-edge))) (error "Invalid delimiter tag ~a" text)) (setf *open-delimiter* (subseq text 0 left-edge) *close-delimiter* (subseq text (1+ right-edge))))) ;;; Parser (defvar *mustache-tag-table* (make-hash-table)) (defun set-mustache-character (char new-function) (setf (gethash char *mustache-tag-table*) new-function)) (defun get-mustache-character (char) (declare (inline)) (gethash char *mustache-tag-table*)) (defun make-tag (&key str escapep start end) (declare (type simple-string str) (type boolean escapep) (type offset start end)) (let* ((tag-fun (get-mustache-character (char str 0))) (tag-text (string-trim '(#\Space #\Tab) str)) (arg-text (string-trim '(#\Space #\Tab) (subseq tag-text 1)))) (if tag-fun (funcall tag-fun str arg-text escapep start end) (make-instance 'normal-tag :text tag-text :escape escapep)))) (defmacro define-mustache-character (char &body body) `(set-mustache-character ,char (lambda (raw-text arg-text escapep start end) (declare (ignorable raw-text arg-text escapep start end) (type simple-string raw-text arg-text) (boolean escapep) (offset start end)) ,@body))) (define-mustache-character #\& (make-instance 'ampersand-tag :text arg-text)) (define-mustache-character #\# (make-instance 'section-start-tag :text arg-text :end end :open-delimiter *open-delimiter* :close-delimiter *close-delimiter*)) (define-mustache-character #\^ (make-instance 'section-start-tag :text arg-text :end end :falsey t)) (define-mustache-character #\/ (make-instance 'section-end-tag :text arg-text :start start)) (define-mustache-character #\! (make-instance 'comment-tag :text "")) (define-mustache-character #\= (let ((arg-text (string-trim '(#\Space #\Tab #\=) arg-text))) (prog1 (make-instance 'delimiter-tag :text arg-text) (change-delimiter arg-text)))) (define-mustache-character #\> (make-instance 'partial-tag :text arg-text)) (defmethod print-object ((object tag) stream) (print-unreadable-object (object stream :type t :identity t) (princ (text object) stream))) ;;; Lexer ;; Invariant token (defvar beginning-of-line (make-instance 'beginning-of-line)) (defvar newline (make-instance 'newline)) (defvar crlf-newline (make-instance 'crlf-newline)) (defun string-starts-with-p (pattern string start) (declare (type string pattern string) (type offset start) (inline)) (eql (string<= pattern string :start2 start) (length pattern))) (defun read-text (type string start end) (declare (type symbol type) (type string string) (offset start end)) (loop :for idx :from start :below end :while (case type (text (text-char-p (char string idx))) (whitespace (space-char-p (char string idx)))) :until (string-starts-with-p *open-delimiter* string idx) :finally (return (values (make-instance type :text (subseq string start idx)) idx)))) (defun read-newline (string start) (declare (type string string) (type offset start)) (if (string-starts-with-p crlf string start) (values crlf-newline (+ 2 start)) (values newline (1+ start)))) (defun read-tag (string triplep start end) (declare (type string string) (type boolean triplep) (type offset start end)) (let ((before-tag start) (tag-open (if triplep *triple-open-delimiter* *open-delimiter*)) (tag-close (if triplep *triple-close-delimiter* *close-delimiter*))) (when (string-starts-with-p tag-open string start) (incf start (length tag-open)) (loop :for idx :from start :below end :until (string-starts-with-p tag-close string idx) :finally (let ((endpos (+ idx (length tag-close)))) (return (values (make-tag :str (subseq string start idx) :escapep (not triplep) :start before-tag :end endpos) endpos))))))) (defun read-token (string start end) (declare (type string string) (type offset start end)) (let ((char (char string start))) (cond ((space-char-p char) (read-text 'whitespace string start end)) ((newline-char-p char) (read-newline string start)) ((string-starts-with-p *triple-open-delimiter* string start) (read-tag string t start end)) ((string-starts-with-p *open-delimiter* string start) (read-tag string nil start end)) (t (read-text 'text string start end))))) (defun scan (string &optional (start 0) (end (length string))) (declare (type string string) (type offset start end)) (let ((*open-delimiter* *default-open-delimiter*) (*close-delimiter* *default-close-delimiter*)) (loop :with idx :of-type offset := start :with token :while (> end idx) :when (zerop idx) :collect beginning-of-line :do (multiple-value-setq (token idx) (read-token string idx end)) :collect token :when (and (< idx end) (typep token 'newline)) :collect beginning-of-line))) ;;; Parser (deftype text-token () '(and token (not (or beginning-of-line can-standalone-tag newline whitespace)))) (defun collect-line (tokens) (declare (type list tokens)) (flet ((newlinep (token) (typep token 'newline))) (loop :for start := 0 :then (1+ finish) :for finish := (position-if #'newlinep tokens :start start) :when (subseq tokens start (and finish (1+ finish))) :collect it :until (null finish)))) (defun tokens-standalone-p (tokens) (declare (type list tokens)) (when (eq (car tokens) beginning-of-line) (loop :for token :in tokens :count (typep token 'can-standalone-tag) :into tags :count (typep token 'text-token) :into texts :finally (return (and (= 1 tags) (= 0 texts)))))) (defun find-standalone-tag (tokens) (declare (type list tokens)) (flet ((tagp (token) (typep token 'tag))) (let* ((pos (position-if #'tagp tokens)) (tag (elt tokens pos))) (setf (indent tag) (remove beginning-of-line (subseq tokens 0 pos))) tag))) (defun trim-standalone (tokens) (declare (type list tokens)) (loop :for line :in (collect-line tokens) :append (if (tokens-standalone-p line) (list (find-standalone-tag line)) line))) (defun make-section-tag (start-tag end-tag tokens) (declare (type tag start-tag end-tag) (type list tokens)) (make-instance 'section-tag :tokens tokens :text (text start-tag) :falsey (falsey start-tag) :start (end start-tag) :end (start end-tag) :open-delimiter (open-delimiter start-tag) :close-delimiter (close-delimiter start-tag))) (defun group-sections (tokens &optional sections acc) (declare (type list tokens sections acc)) (labels ((push-group (acc) (cons nil acc)) (push-token (token acc) (cons (cons token (car acc)) (cdr acc))) (pop-group (acc) (cdr acc)) (top-group (acc) (reverse (car acc))) (push-section-tag (start-tag end-tag acc) (push-token (make-section-tag start-tag end-tag (top-group acc)) (pop-group acc))) (tag-match (tag1 tag2) (string-equal (text tag1) (text tag2)))) (if (not tokens) (top-group acc) (let ((token (car tokens)) (rest (cdr tokens)) (start-tag (car sections))) (typecase token (section-start-tag (group-sections rest (cons token sections) (push-group acc))) (section-end-tag (when (tag-match token start-tag) (group-sections rest (cdr sections) (push-section-tag start-tag token acc)))) (otherwise (group-sections rest sections (push-token token acc)))))))) (defun fold-text (tokens) (declare (type list tokens)) (flet ((textp (token) (typep token 'text))) (loop :for start := 0 :then next :for finish := (position-if-not #'textp tokens :start start) :for next := (and finish (position-if #'textp tokens :start finish)) :for texts := (subseq tokens start finish) :when texts :collect (make-instance 'text :text (format nil "~{~a~}" (mapcar #'text texts))) :when (and finish (subseq tokens finish next)) :append it :while next))) (defun parse (template) (declare (inline)) (group-sections (fold-text (trim-standalone (scan template))))) ;;; Context (defvar *context* nil "Current context for lambda section") (defclass context () ((%data :initarg :data :initform nil :accessor data) (%indent :type list :initarg :indent :initform nil :accessor indent) (%partials :initarg :partials :initform nil :accessor partials) (%next :type (or null context) :initarg :next :initform nil :accessor next))) (defun parse-key (string) (declare (type string string)) (cond ((string= string ".") 'implicit-iterator) (t (loop :for start := 0 :then (1+ finish) :for finish := (position #\. string :start start) :collect (string-upcase (subseq string start finish)) :until (null finish))))) (defun key (token) (check-type token token) (parse-key (text token))) (deftype alist () '(cons (cons atom) (or cons null))) (defun save-hash-table (source) (typecase source (null) (string source) (alist (loop :with table := (make-hash-table :test 'equal) :for (key . value) :in (reverse source) :do (setf (gethash (string-upcase key) table) (save-hash-table value)) :finally (return table))) (sequence (let ((result (map 'vector #'save-hash-table source))) (when (plusp (length result)) result))) (otherwise source))) (defun make-context-chain (data context) (declare (type context context)) (let ((ctx (make-instance 'context))) (setf (data ctx) data (indent ctx) (indent context) (partials ctx) (partials context) (next ctx) context) ctx)) (defun ensure-context (maybe-context) "Ensure MAYBE-CONTEXT is a valid context. If not then make one." (ctypecase maybe-context (list (make-instance 'context :data (save-hash-table maybe-context))) (hash-table (make-instance 'context :data maybe-context)) (context maybe-context))) (defgeneric context-get (key context) (:documentation "Get data from CONTEXT by KEY.")) (defmethod context-get ((key string) (context null)) (declare (ignore key)) (values)) (defmethod context-get ((key string) (context hash-table)) (gethash (string-upcase key) context)) (defmethod context-get ((key string) context) (multiple-value-bind (data find) (context-get key (data context)) (if find (values data find) (when (next context) (context-get key (next context)))))) (defmethod context-get ((key (eql 'implicit-iterator)) context) (values (data context) t)) (defmethod context-get ((key list) context) (multiple-value-bind (data find) (context-get (car key) context) (if (cdr key) (context-get (cdr key) data) (values data find)))) ;;; Partials (defvar *load-path* (list *default-pathname-defaults*) "A list. The search pathes for partials.") (defvar *default-pathname-type* "mustache" "The default file extension for partials.") (defun filename (filename) (declare (type (or string pathname) filename)) (or (uiop:file-exists-p filename) (uiop:file-exists-p (make-pathname :type *default-pathname-type* :defaults filename)))) (defun locate-file (filename) (declare (type (or string pathname) filename)) (uiop:ensure-pathname filename :want-file t) (labels ((filename (path filename) (merge-pathnames path (make-pathname :type *default-pathname-type* :defaults filename))) (dir-file-exists-p (path) (uiop:file-exists-p (filename path filename)))) (some #'dir-file-exists-p *load-path*))) (defun read-partial (filename context) (declare (type (or string pathname) filename) (type context context)) (let ((from-context (context-get filename (partials context)))) (if from-context from-context (let ((pathname (locate-file filename))) (when pathname (uiop:read-file-string pathname)))))) ;;; Rendering Utils (defun escape (string) "HTML escape STRING." (declare (type string string)) (flet ((needs-escape-p (char) (member char '(#\< #\> #\& #\\ #\" #\'))) (escape-char (char) (case char (#\& "&") (#\< "<") (#\> ">") (#\" """) (t (format nil "&#~d;" (char-code char)))))) (with-output-to-string (datum) (loop :for start := 0 :then (1+ pos) :for pos := (position-if #'needs-escape-p string :start start) :do (write-string string datum :start start :end pos) :when pos :do (write-string (escape-char (char string pos)) datum) :while pos)))) (defvar *real-standard-output* (make-synonym-stream 'cl:*standard-output*)) (defvar *output-stream* (make-synonym-stream 'cl:*standard-output*) "The default output stream for mustache rendering. Bind this variable before calling mustache-rendering and friends. Default is *standard-output*.") (defun %output () (if (eq *mustache-output* *real-standard-output*) *output-stream* *mustache-output*)) (defgeneric print-data (data escapep context)) (defmethod print-data ((data string) escapep context) (declare (ignore context)) (write-string (if escapep (escape data) data) (%output))) (defmethod print-data ((data symbol) escapep context) (print-data (string data) escapep context)) (defmethod print-data ((data function) escapep context) (let ((*context* context)) (let* ((value (format nil "~a" (funcall data))) (fun (compile-template value)) (output (with-output-to-string (*output-stream*) (funcall fun context)))) (write-string (if escapep (escape output) output) (%output))))) (defmethod print-data (token escapep context) (print-data (princ-to-string token) escapep context)) (defun call-lambda (lambda text context) (declare (type function lambda) (type string text) (type context context)) (let ((*context* context)) (let* ((value (format nil "~a" (funcall lambda text))) (fun (compile-template value)) (output (with-output-to-string (*output-stream*) (funcall fun context)))) (write-string output (%output))))) ;;; Renderer (defgeneric render-token (token context template)) (defmethod render-token ((token text) context (template string)) (declare (ignore template)) (print-data (text token) nil context)) (defmethod render-token ((token tag) context (template string)) (declare (ignore template)) (multiple-value-bind (dat find) (context-get (key token) context) (when find (print-data dat (escapep token) context)))) (defmethod render-token ((token partial-tag) context (template string)) (let ((fun (compile-template (or (read-partial (text token) context) "")))) (push (indent token) (indent context)) (funcall fun context) (pop (indent context)))) (defmethod render-token ((token section-tag) context (template string)) (multiple-value-bind (ctx find) (context-get (key token) context) (when (or find (falsey token)) (flet ((render (context template) (render-tokens (tokens token) context template))) (if (falsey token) (when (null ctx) (render (make-context-chain () context) template)) (typecase ctx (hash-table (render (make-context-chain ctx context) template)) (function (let ((*default-open-delimiter* (open-delimiter token)) (*default-close-delimiter* (close-delimiter token))) (call-lambda ctx (subseq template (start token) (end token)) context))) ((and (not string) sequence) (map nil (lambda (ctx) (render (make-context-chain ctx context) template)) ctx)) (null) (t (render context template)))))))) (defmethod render-token ((token beginning-of-line) context (template string)) (declare (ignore token)) (render-tokens (car (indent context)) context template)) (defun render-tokens (tokens context template) (declare (type list tokens) (type context context) (type string template)) (loop :for token :in tokens :do (render-token token context template))) (defun render-body (tokens context template) (let ((context (ensure-context context))) (with-standard-io-syntax (render-tokens tokens context template)))) ;;; Interfaces (defun version () "Return the CL-MUSTACHE version." #.(format nil "CL-MUSTACHE ~A (Mustache spec ~A)" (with-open-file (f (merge-pathnames "version.lisp-expr" (or *compile-file-pathname* *load-truename*))) (read f)) (with-open-file (f (merge-pathnames "spec-version.lisp-expr" (or *compile-file-pathname* *load-truename*))) (read f)))) (defun make-context (&key data partials) "Create mustache context from alist DATA." (make-instance 'context :data (save-hash-table data) :partials (save-hash-table partials))) (defgeneric compile-template (template) (:documentation "Return a compiled rendering function.")) (defmethod compile-template ((template string)) (let ((tokens (parse template))) (lambda (&optional context output-stream) (let ((*output-stream* (or output-stream *output-stream*))) (render-body tokens context template))))) (defmethod compile-template ((template pathname)) (let ((buffer (uiop:read-file-string (filename template)))) (compile-template buffer))) (defgeneric render (template &optional context output-stream) (:documentation "Render TEMPLATE with optional CONTEXT to *OUTPUT-STREAM* or OUTPUT-STREAM")) (defmethod render ((template string) &optional context output-stream) (let ((*output-stream* (or output-stream *output-stream*))) (render-body (parse template) context template))) (defmethod render ((template pathname) &optional context output-stream) (let ((buffer (uiop:read-file-string (filename template)))) (render buffer context output-stream))) (defun render* (template &optional context) "Render TEMPLATE with optional CONTEXT to string." (with-output-to-string (out) (render template context out))) (defmacro define (name template) "Define a named renderer of string TEMPLATE." `(setf (symbol-function ',name) (compile-template ,template))) ;;; mustache.lisp ends here ;;; Local Variables: ;;; mode: lisp ;;; End: cl-mustache/packages.lisp000066400000000000000000000035661257077450200157550ustar00rootroot00000000000000;;;; packages.lisp --- Packages of cl-mustache ;;; Copyright (C) 2012 Kan-Ru Chen ;;; Author: Kan-Ru Chen ;;; Permission is hereby granted, free of charge, to any person obtaining a copy of ;;; this software and associated documentation files (the "Software"), to deal in ;;; the Software without restriction, including without limitation the rights to ;;; use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies ;;; of the Software, and to permit persons to whom the Software is furnished to do ;;; so, subject to the following conditions: ;;; The above copyright notice and this permission notice shall be included in all ;;; copies or substantial portions of the Software. ;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ;;; SOFTWARE. ;;;; Commentary: ;;; ;;;; Code: (in-package #:cl-user) (defpackage #:mustache (:use #:cl) (:export #:*load-path* #:*default-pathname-type* ;; new #:*output-stream* #:*context* #:version #:make-context #:compile-template #:render #:render* #:define ;; old #:*mustache-output* #:mustache-type #:mustache-version #:mustache-context #:mustache-compile #:mustache-render #:mustache-render-to-string #:mustache-render-to-stream #:defmustache)) ;;; packages.lisp ends here cl-mustache/spec-version.lisp-expr000066400000000000000000000000531257077450200175540ustar00rootroot00000000000000;; -*- lisp -*- "1.1.2, including lambdas" cl-mustache/t/000077500000000000000000000000001257077450200135375ustar00rootroot00000000000000cl-mustache/t/ci.lisp000066400000000000000000000034611257077450200150270ustar00rootroot00000000000000;;;; ci.lisp --- CI Script ;;; Copyright (C) 2013 Kan-Ru Chen (陳侃如) ;;; Author(s): Kan-Ru Chen (陳侃如) ;;; Permission is hereby granted, free of charge, to any person obtaining a ;;; copy of this software and associated documentation files (the "Software"), ;;; to deal in the Software without restriction, including without limitation ;;; the rights to use, copy, modify, merge, publish, distribute, sublicense, ;;; and/or sell copies of the Software, and to permit persons to whom the ;;; Software is furnished to do so, subject to the following conditions: ;;; The above copyright notice and this permission notice shall be included in ;;; all copies or substantial portions of the Software. ;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING ;;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER ;;; DEALINGS IN THE SOFTWARE. ;;;; Commentary: ;;; ;;;; Code: (in-package :cl-user) (defpackage :mustache-test-ci (:use :cl)) (in-package :mustache-test-ci) (defun exit (code) (let ((exit (find-symbol #.(string :exit) :sb-ext)) (quit (find-symbol #.(string :quit) :sb-ext))) (cond (exit (funcall exit :code code)) (quit (funcall quit :unix-status code)) (t (error "No exit or quit function"))))) (ql:quickload :prove) (ql:quickload :cl-mustache) (unless (and (prove:run #P"t/test-api") (prove:run #P"t/test-spec")) (exit -1)) ;;; ci.lisp ends here ;;; Local Variables: ;;; mode: lisp ;;; End: cl-mustache/t/gen-test-spec.lisp000066400000000000000000000066131257077450200171140ustar00rootroot00000000000000;;;; gen-test-spec.lisp --- Test against the specs ;;; Copyright (C) 2011, 2012 Kan-Ru Chen ;;; Author: Kan-Ru Chen ;;; Permission is hereby granted, free of charge, to any person obtaining a copy of ;;; this software and associated documentation files (the "Software"), to deal in ;;; the Software without restriction, including without limitation the rights to ;;; use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies ;;; of the Software, and to permit persons to whom the Software is furnished to do ;;; so, subject to the following conditions: ;;; The above copyright notice and this permission notice shall be included in all ;;; copies or substantial portions of the Software. ;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ;;; SOFTWARE. ;;;; Commentary: ;;;; Code: (in-package :cl-user) (require :alexandria) (require :cl-fad) (require :cl-json) (defparameter *spec-directory* (make-pathname :directory (append (pathname-directory #.(or *load-truename* *compile-file-truename*)) '("spec" "specs")))) (defun utf8-json-decode (pathname) (with-open-file (stream pathname :direction :input) (let ((json:*json-array-type* 'vector)) (json:decode-json-from-source stream)))) (defun json-file-p (pathname) (string= "json" (pathname-type pathname))) (defun all-specs () (let (specs) (uiop:collect-sub*directories *spec-directory* (constantly t) (complement #'uiop:hidden-pathname-p) (lambda (dir) (mapc (lambda (file) (when (json-file-p file) (push (utf8-json-decode file) specs))) (uiop:directory-files dir)))) specs)) (defmacro with-test ((test) &body body) `(let ((name (alexandria:assoc-value ,test :name)) (template (alexandria:assoc-value ,test :template)) (data (alexandria:assoc-value ,test :data)) (expected (alexandria:assoc-value ,test :expected)) (desc (alexandria:assoc-value ,test :desc)) (partials (alexandria:assoc-value ,test :partials))) ,@body)) (defmacro with-test-in-specs ((test) specs &body body) (alexandria:with-gensyms (spec) `(loop for ,spec in ,specs do (loop for ,test across (alexandria:assoc-value ,spec :tests) do (progn ,@body))))) ;; Generate test file (let ((*print-case* :downcase)) (pprint '(in-package :mustache-test)) (pprint `(deftest spec ,@(let (tests) (with-test-in-specs (test) (all-specs) (with-test (test) (push `(is (mustache:render* ,template (mustache:make-context :data ',data :partials ',partials)) ,expected (format nil "~A :: ~A" ,name ,desc)) tests))) tests) (finalize)))) ;;; gen-test-spec.lisp ends here ;;; Local Variables: ;;; mode: lisp ;;; End: cl-mustache/t/run-test.sh000077500000000000000000000011131257077450200156530ustar00rootroot00000000000000#!/bin/sh if [ ! -e /usr/share/cl-quicklisp/quicklisp.lisp ]; then echo "Must install cl-quicklisp to run tests"; exit -1; fi if [ ! -e t/ci.lisp ]; then echo "Must run this from top-level"; exit -1; fi export QLDIR=`mktemp -d`/ sbcl --no-userinit \ --non-interactive \ --load /usr/share/cl-quicklisp/quicklisp.lisp \ --eval "(require 'sb-posix)" \ --eval "(quicklisp-quickstart:install :path (sb-posix:getenv \"QLDIR\"))" ln -s $PWD $QLDIR/local-projects/ sbcl --load $QLDIR/setup.lisp \ --script t/ci.lisp rv=$? rm -rf $QLDIR exit $rv cl-mustache/t/test-api.lisp000066400000000000000000000076211257077450200161640ustar00rootroot00000000000000;;;; test-api.lisp --- CL-MUSTACHE API test ;;; Copyright (C) 2012 Kan-Ru Chen (陳侃如) ;;; Author(s): Kan-Ru Chen (陳侃如) ;;; Permission is hereby granted, free of charge, to any person obtaining a ;;; copy of this software and associated documentation files (the "Software"), ;;; to deal in the Software without restriction, including without limitation ;;; the rights to use, copy, modify, merge, publish, distribute, sublicense, ;;; and/or sell copies of the Software, and to permit persons to whom the ;;; Software is furnished to do so, subject to the following conditions: ;;; The above copyright notice and this permission notice shall be included in ;;; all copies or substantial portions of the Software. ;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING ;;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER ;;; DEALINGS IN THE SOFTWARE. ;;;; Commentary: ;;; ;;;; Code: (in-package :cl-user) (defpackage #:mustache-test-api (:use #:cl #:prove)) (in-package :mustache-test-api) (defclass unreadable-class () ()) (plan 14) (is-type (mustache:version) 'string "(mustache:version) is a version string") (is-type (mustache:compile-template "string") 'function "compile a string template") (is-type (mustache:compile-template (make-pathname :directory (pathname-directory #.(or *load-truename* *compile-file-truename*)) :name "test" :type "mustache")) 'function "compile a file template") (is (with-output-to-string (mustache:*output-stream*) (mustache:render "TEMPLATE")) "TEMPLATE" "render to mustache:*output-stream*") (is (mustache:render* "TEMPLATE") "TEMPLATE" "render to string") (is (with-output-to-string (out) (mustache:render "TEMPLATE" nil out)) "TEMPLATE" "render to out stream") (mustache:define test-mustache-define "TEMPLATE") (is (with-output-to-string (mustache:*output-stream*) (test-mustache-define)) "TEMPLATE" "mustache:define works") (is (mustache:render* "{{#list}}{{item}}{{/list}}" '((list . (((item . "a")) ((item . "b")) ((item . "c")))))) "abc" "render context from list") (is (mustache:render* "{{var}}" `((:var . ,(make-array 0 :element-type 'character :adjustable t :fill-pointer 0)))) "" "render a string of type '(array character (*))") (is (mustache:render* "{{var}}" (let ((context (make-hash-table :test #'equal))) (setf (gethash "VAR" context) "test") context)) "test" "use a hash-table as context.") (is (mustache:render* "{{escape}}" '((escape . "<>&\"'"))) "<>&"'" "escape char") (is (mustache:render* "{{var}}" '((var . "pass") (var . "fail"))) "pass" "use alist as context, the first match should shadow the rest.") (let ((unreadable-instance (make-instance 'unreadable-class))) (is (mustache:render* "!!{{unknown-type}}!!" `((unknown-type . ,unreadable-instance))) (format nil "!!~a!!" (mustache::escape (princ-to-string unreadable-instance))) "escape non printable types")) (is (mustache:render* "{{symbol}}" '((symbol . symbol))) "SYMBOL" "print symbols") (finalize) ;;; test-api.lisp ends here ;;; Local Variables: ;;; mode: lisp ;;; End: cl-mustache/t/test-spec.lisp000066400000000000000000001126131257077450200163430ustar00rootroot00000000000000;;;; Auto-generated from mustache spec (in-package :cl-user) (defpackage #:mustache-test-spec (:use #:cl #:prove)) (in-package :mustache-test-spec) (plan 124) (is (mustache:render* "12345 {{! Comment Block! }} 67890" (mustache:make-context :data 'nil :partials 'nil)) "12345 67890" (format nil "~A :: ~A" "Surrounding Whitespace" "Comment removal should preserve surrounding whitespace.")) (is (mustache:render* " 12 {{! 34 }} " (mustache:make-context :data 'nil :partials 'nil)) " 12 " (format nil "~A :: ~A" "Indented Inline" "Inline comments should not strip whitespace")) (is (mustache:render* "Begin. {{! Something's going on here... }} End. " (mustache:make-context :data 'nil :partials 'nil)) "Begin. End. " (format nil "~A :: ~A" "Indented Multiline Standalone" "All standalone comment lines should be removed.")) (is (mustache:render* "Begin. {{! Something's going on here... }} End. " (mustache:make-context :data 'nil :partials 'nil)) "Begin. End. " (format nil "~A :: ~A" "Multiline Standalone" "All standalone comment lines should be removed.")) (is (mustache:render* "! {{! I'm Still Standalone }}" (mustache:make-context :data 'nil :partials 'nil)) "! " (format nil "~A :: ~A" "Standalone Without Newline" "Standalone tags should not require a newline to follow them.")) (is (mustache:render* " {{! I'm Still Standalone }} !" (mustache:make-context :data 'nil :partials 'nil)) "!" (format nil "~A :: ~A" "Standalone Without Previous Line" "Standalone tags should not require a newline to precede them.")) (is (mustache:render* "| {{! Standalone Comment }} |" (mustache:make-context :data 'nil :partials 'nil)) "| |" (format nil "~A :: ~A" "Standalone Line Endings" "\"\\r\\n\" should be considered a newline for standalone tags.")) (is (mustache:render* "Begin. {{! Indented Comment Block! }} End. " (mustache:make-context :data 'nil :partials 'nil)) "Begin. End. " (format nil "~A :: ~A" "Indented Standalone" "All standalone comment lines should be removed.")) (is (mustache:render* "Begin. {{! Comment Block! }} End. " (mustache:make-context :data 'nil :partials 'nil)) "Begin. End. " (format nil "~A :: ~A" "Standalone" "All standalone comment lines should be removed.")) (is (mustache:render* "12345{{! This is a multi-line comment... }}67890 " (mustache:make-context :data 'nil :partials 'nil)) "1234567890 " (format nil "~A :: ~A" "Multiline" "Multiline comments should be permitted.")) (is (mustache:render* "12345{{! Comment Block! }}67890" (mustache:make-context :data 'nil :partials 'nil)) "1234567890" (format nil "~A :: ~A" "Inline" "Comment blocks should be removed from the template.")) (is (mustache:render* "|{{= @ @ =}}|" (mustache:make-context :data 'nil :partials 'nil)) "||" (format nil "~A :: ~A" "Pair with Padding" "Superfluous in-tag whitespace should be ignored.")) (is (mustache:render* "= {{=@ @=}}" (mustache:make-context :data 'nil :partials 'nil)) "= " (format nil "~A :: ~A" "Standalone Without Newline" "Standalone tags should not require a newline to follow them.")) (is (mustache:render* " {{=@ @=}} =" (mustache:make-context :data 'nil :partials 'nil)) "=" (format nil "~A :: ~A" "Standalone Without Previous Line" "Standalone tags should not require a newline to precede them.")) (is (mustache:render* "| {{= @ @ =}} |" (mustache:make-context :data 'nil :partials 'nil)) "| |" (format nil "~A :: ~A" "Standalone Line Endings" "\"\\r\\n\" should be considered a newline for standalone tags.")) (is (mustache:render* "Begin. {{=@ @=}} End. " (mustache:make-context :data 'nil :partials 'nil)) "Begin. End. " (format nil "~A :: ~A" "Indented Standalone Tag" "Indented standalone lines should be removed from the template.")) (is (mustache:render* "Begin. {{=@ @=}} End. " (mustache:make-context :data 'nil :partials 'nil)) "Begin. End. " (format nil "~A :: ~A" "Standalone Tag" "Standalone lines should be removed from the template.")) (is (mustache:render* " | {{=@ @=}} " (mustache:make-context :data 'nil :partials 'nil)) " | " (format nil "~A :: ~A" "Outlying Whitespace (Inline)" "Whitespace should be left untouched.")) (is (mustache:render* "| {{=@ @=}} |" (mustache:make-context :data 'nil :partials 'nil)) "| |" (format nil "~A :: ~A" "Surrounding Whitespace" "Surrounding whitespace should be left untouched.")) (is (mustache:render* "[ {{>include}} ] [ .{{value}}. .|value|. ] " (mustache:make-context :data '((:value . "yes")) :partials '((:include . ".{{value}}. {{= | | =}} .|value|.")))) "[ .yes. .yes. ] [ .yes. .|value|. ] " (format nil "~A :: ~A" "Post-Partial Behavior" "Delimiters set in a partial should not affect the parent template.")) (is (mustache:render* "[ {{>include}} ] {{= | | =}} [ |>include| ] " (mustache:make-context :data '((:value . "yes")) :partials '((:include . ".{{value}}.")))) "[ .yes. ] [ .yes. ] " (format nil "~A :: ~A" "Partial Inheritence" "Delimiters set in a parent template should not affect a partial.")) (is (mustache:render* "[ {{^section}} {{data}} |data| {{/section}} {{= | | =}} |^section| {{data}} |data| |/section| ] " (mustache:make-context :data '((:section) (:data . "I got interpolated.")) :partials 'nil)) "[ I got interpolated. |data| {{data}} I got interpolated. ] " (format nil "~A :: ~A" "Inverted Sections" "Delimiters set outside inverted sections should persist.")) (is (mustache:render* "[ {{#section}} {{data}} |data| {{/section}} {{= | | =}} |#section| {{data}} |data| |/section| ] " (mustache:make-context :data '((:section . t) (:data . "I got interpolated.")) :partials 'nil)) "[ I got interpolated. |data| {{data}} I got interpolated. ] " (format nil "~A :: ~A" "Sections" "Delimiters set outside sections should persist.")) (is (mustache:render* "({{=[ ]=}}[text])" (mustache:make-context :data '((:text . "It worked!")) :partials 'nil)) "(It worked!)" (format nil "~A :: ~A" "Special Characters" "Characters with special meaning regexen should be valid delimiters.")) (is (mustache:render* "{{=<% %>=}}(<%text%>)" (mustache:make-context :data '((:text . "Hey!")) :partials 'nil)) "(Hey!)" (format nil "~A :: ~A" "Pair Behavior" "The equals sign (used on both sides) should permit delimiter changes.")) (is (mustache:render* "|{{& string }}|" (mustache:make-context :data '((:string . "---")) :partials 'nil)) "|---|" (format nil "~A :: ~A" "Ampersand With Padding" "Superfluous in-tag whitespace should be ignored.")) (is (mustache:render* "|{{{ string }}}|" (mustache:make-context :data '((:string . "---")) :partials 'nil)) "|---|" (format nil "~A :: ~A" "Triple Mustache With Padding" "Superfluous in-tag whitespace should be ignored.")) (is (mustache:render* "|{{ string }}|" (mustache:make-context :data '((:string . "---")) :partials 'nil)) "|---|" (format nil "~A :: ~A" "Interpolation With Padding" "Superfluous in-tag whitespace should be ignored.")) (is (mustache:render* " {{&string}} " (mustache:make-context :data '((:string . "---")) :partials 'nil)) " --- " (format nil "~A :: ~A" "Ampersand - Standalone" "Standalone interpolation should not alter surrounding whitespace.")) (is (mustache:render* " {{{string}}} " (mustache:make-context :data '((:string . "---")) :partials 'nil)) " --- " (format nil "~A :: ~A" "Triple Mustache - Standalone" "Standalone interpolation should not alter surrounding whitespace.")) (is (mustache:render* " {{string}} " (mustache:make-context :data '((:string . "---")) :partials 'nil)) " --- " (format nil "~A :: ~A" "Interpolation - Standalone" "Standalone interpolation should not alter surrounding whitespace.")) (is (mustache:render* "| {{&string}} |" (mustache:make-context :data '((:string . "---")) :partials 'nil)) "| --- |" (format nil "~A :: ~A" "Ampersand - Surrounding Whitespace" "Interpolation should not alter surrounding whitespace.")) (is (mustache:render* "| {{{string}}} |" (mustache:make-context :data '((:string . "---")) :partials 'nil)) "| --- |" (format nil "~A :: ~A" "Triple Mustache - Surrounding Whitespace" "Interpolation should not alter surrounding whitespace.")) (is (mustache:render* "| {{string}} |" (mustache:make-context :data '((:string . "---")) :partials 'nil)) "| --- |" (format nil "~A :: ~A" "Interpolation - Surrounding Whitespace" "Interpolation should not alter surrounding whitespace.")) (is (mustache:render* "\"{{#a}}{{b.c.d.e.name}}{{/a}}\" == \"Phil\"" (mustache:make-context :data '((:a (:b (:c (:d (:e (:name . "Phil")))))) (:b (:c (:d (:e (:name . "Wrong")))))) :partials 'nil)) "\"Phil\" == \"Phil\"" (format nil "~A :: ~A" "Dotted Names - Initial Resolution" "The first part of a dotted name should resolve as any other name.")) (is (mustache:render* "\"{{a.b.c.name}}\" == \"\"" (mustache:make-context :data '((:a (:b)) (:c (:name . "Jim"))) :partials 'nil)) "\"\" == \"\"" (format nil "~A :: ~A" "Dotted Names - Broken Chain Resolution" "Each part of a dotted name should resolve only against its parent.")) (is (mustache:render* "\"{{a.b.c}}\" == \"\"" (mustache:make-context :data '((:a)) :partials 'nil)) "\"\" == \"\"" (format nil "~A :: ~A" "Dotted Names - Broken Chains" "Any falsey value prior to the last part of the name should yield ''.")) (is (mustache:render* "\"{{a.b.c.d.e.name}}\" == \"Phil\"" (mustache:make-context :data '((:a (:b (:c (:d (:e (:name . "Phil"))))))) :partials 'nil)) "\"Phil\" == \"Phil\"" (format nil "~A :: ~A" "Dotted Names - Arbitrary Depth" "Dotted names should be functional to any level of nesting.")) (is (mustache:render* "\"{{&person.name}}\" == \"{{#person}}{{&name}}{{/person}}\"" (mustache:make-context :data '((:person (:name . "Joe"))) :partials 'nil)) "\"Joe\" == \"Joe\"" (format nil "~A :: ~A" "Dotted Names - Ampersand Interpolation" "Dotted names should be considered a form of shorthand for sections.")) (is (mustache:render* "\"{{{person.name}}}\" == \"{{#person}}{{{name}}}{{/person}}\"" (mustache:make-context :data '((:person (:name . "Joe"))) :partials 'nil)) "\"Joe\" == \"Joe\"" (format nil "~A :: ~A" "Dotted Names - Triple Mustache Interpolation" "Dotted names should be considered a form of shorthand for sections.")) (is (mustache:render* "\"{{person.name}}\" == \"{{#person}}{{name}}{{/person}}\"" (mustache:make-context :data '((:person (:name . "Joe"))) :partials 'nil)) "\"Joe\" == \"Joe\"" (format nil "~A :: ~A" "Dotted Names - Basic Interpolation" "Dotted names should be considered a form of shorthand for sections.")) (is (mustache:render* "I ({{&cannot}}) be seen!" (mustache:make-context :data 'nil :partials 'nil)) "I () be seen!" (format nil "~A :: ~A" "Ampersand Context Miss Interpolation" "Failed context lookups should default to empty strings.")) (is (mustache:render* "I ({{{cannot}}}) be seen!" (mustache:make-context :data 'nil :partials 'nil)) "I () be seen!" (format nil "~A :: ~A" "Triple Mustache Context Miss Interpolation" "Failed context lookups should default to empty strings.")) (is (mustache:render* "I ({{cannot}}) be seen!" (mustache:make-context :data 'nil :partials 'nil)) "I () be seen!" (format nil "~A :: ~A" "Basic Context Miss Interpolation" "Failed context lookups should default to empty strings.")) (is (with-standard-io-syntax (mustache:render* "\"{{&power}} jiggawatts!\"" (mustache:make-context :data '((:power . 1.21f0)) :partials 'nil))) "\"1.21 jiggawatts!\"" (format nil "~A :: ~A" "Ampersand Decimal Interpolation" "Decimals should interpolate seamlessly with proper significance.")) (is (with-standard-io-syntax (mustache:render* "\"{{{power}}} jiggawatts!\"" (mustache:make-context :data '((:power . 1.21f0)) :partials 'nil))) "\"1.21 jiggawatts!\"" (format nil "~A :: ~A" "Triple Mustache Decimal Interpolation" "Decimals should interpolate seamlessly with proper significance.")) (is (with-standard-io-syntax (mustache:render* "\"{{power}} jiggawatts!\"" (mustache:make-context :data '((:power . 1.21f0)) :partials 'nil))) "\"1.21 jiggawatts!\"" (format nil "~A :: ~A" "Basic Decimal Interpolation" "Decimals should interpolate seamlessly with proper significance.")) (is (mustache:render* "\"{{&mph}} miles an hour!\"" (mustache:make-context :data '((:mph . 85)) :partials 'nil)) "\"85 miles an hour!\"" (format nil "~A :: ~A" "Ampersand Integer Interpolation" "Integers should interpolate seamlessly.")) (is (mustache:render* "\"{{{mph}}} miles an hour!\"" (mustache:make-context :data '((:mph . 85)) :partials 'nil)) "\"85 miles an hour!\"" (format nil "~A :: ~A" "Triple Mustache Integer Interpolation" "Integers should interpolate seamlessly.")) (is (mustache:render* "\"{{mph}} miles an hour!\"" (mustache:make-context :data '((:mph . 85)) :partials 'nil)) "\"85 miles an hour!\"" (format nil "~A :: ~A" "Basic Integer Interpolation" "Integers should interpolate seamlessly.")) (is (mustache:render* "These characters should not be HTML escaped: {{&forbidden}} " (mustache:make-context :data '((:forbidden . "& \" < >")) :partials 'nil)) "These characters should not be HTML escaped: & \" < > " (format nil "~A :: ~A" "Ampersand" "Ampersand should interpolate without HTML escaping.")) (is (mustache:render* "These characters should not be HTML escaped: {{{forbidden}}} " (mustache:make-context :data '((:forbidden . "& \" < >")) :partials 'nil)) "These characters should not be HTML escaped: & \" < > " (format nil "~A :: ~A" "Triple Mustache" "Triple mustaches should interpolate without HTML escaping.")) (is (mustache:render* "These characters should be HTML escaped: {{forbidden}} " (mustache:make-context :data '((:forbidden . "& \" < >")) :partials 'nil)) "These characters should be HTML escaped: & " < > " (format nil "~A :: ~A" "HTML Escaping" "Basic interpolation should be HTML escaped.")) (is (mustache:render* "Hello, {{subject}}! " (mustache:make-context :data '((:subject . "world")) :partials 'nil)) "Hello, world! " (format nil "~A :: ~A" "Basic Interpolation" "Unadorned tags should interpolate content into the template.")) (is (mustache:render* "Hello from {Mustache}! " (mustache:make-context :data 'nil :partials 'nil)) "Hello from {Mustache}! " (format nil "~A :: ~A" "No Interpolation" "Mustache-free templates should render as-is.")) (is (mustache:render* "|{{^ boolean }}={{/ boolean }}|" (mustache:make-context :data '((:boolean)) :partials 'nil)) "|=|" (format nil "~A :: ~A" "Padding" "Superfluous in-tag whitespace should be ignored.")) (is (mustache:render* "^{{^boolean}} / {{/boolean}}" (mustache:make-context :data '((:boolean)) :partials 'nil)) "^ / " (format nil "~A :: ~A" "Standalone Without Newline" "Standalone tags should not require a newline to follow them.")) (is (mustache:render* " {{^boolean}} ^{{/boolean}} /" (mustache:make-context :data '((:boolean)) :partials 'nil)) "^ /" (format nil "~A :: ~A" "Standalone Without Previous Line" "Standalone tags should not require a newline to precede them.")) (is (mustache:render* "| {{^boolean}} {{/boolean}} |" (mustache:make-context :data '((:boolean)) :partials 'nil)) "| |" (format nil "~A :: ~A" "Standalone Line Endings" "\"\\r\\n\" should be considered a newline for standalone tags.")) (is (mustache:render* "| This Is {{^boolean}} | {{/boolean}} | A Line " (mustache:make-context :data '((:boolean)) :partials 'nil)) "| This Is | | A Line " (format nil "~A :: ~A" "Standalone Indented Lines" "Standalone indented lines should be removed from the template.")) (is (mustache:render* "| This Is {{^boolean}} | {{/boolean}} | A Line " (mustache:make-context :data '((:boolean)) :partials 'nil)) "| This Is | | A Line " (format nil "~A :: ~A" "Standalone Lines" "Standalone lines should be removed from the template.")) (is (mustache:render* " {{^boolean}}NO{{/boolean}} {{^boolean}}WAY{{/boolean}} " (mustache:make-context :data '((:boolean)) :partials 'nil)) " NO WAY " (format nil "~A :: ~A" "Indented Inline Sections" "Single-line sections should not alter surrounding whitespace.")) (is (mustache:render* " | {{^boolean}} {{! Important Whitespace }} {{/boolean}} | " (mustache:make-context :data '((:boolean)) :partials 'nil)) " | | " (format nil "~A :: ~A" "Internal Whitespace" "Inverted should not alter internal whitespace.")) (is (mustache:render* " | {{^boolean}} | {{/boolean}} | " (mustache:make-context :data '((:boolean)) :partials 'nil)) " | | | " (format nil "~A :: ~A" "Surrounding Whitespace" "Inverted sections should not alter surrounding whitespace.")) (is (mustache:render* "\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"" (mustache:make-context :data '((:a)) :partials 'nil)) "\"Not Here\" == \"Not Here\"" (format nil "~A :: ~A" "Dotted Names - Broken Chains" "Dotted names that cannot be resolved should be considered falsey.")) (is (mustache:render* "\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"" (mustache:make-context :data '((:a (:b (:c)))) :partials 'nil)) "\"Not Here\" == \"Not Here\"" (format nil "~A :: ~A" "Dotted Names - Falsey" "Dotted names should be valid for Inverted Section tags.")) (is (mustache:render* "\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"\"" (mustache:make-context :data '((:a (:b (:c . t)))) :partials 'nil)) "\"\" == \"\"" (format nil "~A :: ~A" "Dotted Names - Truthy" "Dotted names should be valid for Inverted Section tags.")) (is (mustache:render* "[{{^missing}}Cannot find key 'missing'!{{/missing}}]" (mustache:make-context :data 'nil :partials 'nil)) "[Cannot find key 'missing'!]" (format nil "~A :: ~A" "Context Misses" "Failed context lookups should be considered falsey.")) (is (mustache:render* "| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |" (mustache:make-context :data '((:bool . t)) :partials 'nil)) "| A E |" (format nil "~A :: ~A" "Nested (Truthy)" "Nested truthy sections should be omitted.")) (is (mustache:render* "| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |" (mustache:make-context :data '((:bool)) :partials 'nil)) "| A B C D E |" (format nil "~A :: ~A" "Nested (Falsey)" "Nested falsey sections should have their contents rendered.")) (is (mustache:render* "{{^bool}} * first {{/bool}} * {{two}} {{^bool}} * third {{/bool}} " (mustache:make-context :data '((:two . "second") (:bool)) :partials 'nil)) "* first * second * third " (format nil "~A :: ~A" "Doubled" "Multiple inverted sections per template should be permitted.")) (is (mustache:render* "\"{{^list}}Yay lists!{{/list}}\"" (mustache:make-context :data '((:list . #())) :partials 'nil)) "\"Yay lists!\"" (format nil "~A :: ~A" "Empty List" "Empty lists should behave like falsey values.")) (is (mustache:render* "\"{{^list}}{{n}}{{/list}}\"" (mustache:make-context :data '((:list . #(((:n . 1)) ((:n . 2)) ((:n . 3))))) :partials 'nil)) "\"\"" (format nil "~A :: ~A" "List" "Lists should behave like truthy values.")) (is (mustache:render* "\"{{^context}}Hi {{name}}.{{/context}}\"" (mustache:make-context :data '((:context (:name . "Joe"))) :partials 'nil)) "\"\"" (format nil "~A :: ~A" "Context" "Objects and hashes should behave like truthy values.")) (is (mustache:render* "\"{{^boolean}}This should not be rendered.{{/boolean}}\"" (mustache:make-context :data '((:boolean . t)) :partials 'nil)) "\"\"" (format nil "~A :: ~A" "Truthy" "Truthy sections should have their contents omitted.")) (is (mustache:render* "\"{{^boolean}}This should be rendered.{{/boolean}}\"" (mustache:make-context :data '((:boolean)) :partials 'nil)) "\"This should be rendered.\"" (format nil "~A :: ~A" "Falsey" "Falsey sections should have their contents rendered.")) (is (mustache:render* "|{{> partial }}|" (mustache:make-context :data '((:boolean . t)) :partials '((:partial . "[]")))) "|[]|" (format nil "~A :: ~A" "Padding Whitespace" "Superfluous in-tag whitespace should be ignored.")) (is (mustache:render* "\\ {{>partial}} / " (mustache:make-context :data '((:content . "< ->")) :partials '((:partial . "| {{{content}}} | ")))) "\\ | < -> | / " (format nil "~A :: ~A" "Standalone Indentation" "Each line of the partial should be indented before rendering.")) (is (mustache:render* "> {{>partial}}" (mustache:make-context :data 'nil :partials '((:partial . "> >")))) "> > >" (format nil "~A :: ~A" "Standalone Without Newline" "Standalone tags should not require a newline to follow them.")) (is (mustache:render* " {{>partial}} >" (mustache:make-context :data 'nil :partials '((:partial . "> >")))) " > >>" (format nil "~A :: ~A" "Standalone Without Previous Line" "Standalone tags should not require a newline to precede them.")) (is (mustache:render* "| {{>partial}} |" (mustache:make-context :data 'nil :partials '((:partial . ">")))) "| >|" (format nil "~A :: ~A" "Standalone Line Endings" "\"\\r\\n\" should be considered a newline for standalone tags.")) (is (mustache:render* " {{data}} {{> partial}} " (mustache:make-context :data '((:data . "|")) :partials '((:partial . "> >")))) " | > > " (format nil "~A :: ~A" "Inline Indentation" "Whitespace should be left untouched.")) (is (mustache:render* "| {{>partial}} |" (mustache:make-context :data 'nil :partials '((:partial . " | ")))) "| | |" (format nil "~A :: ~A" "Surrounding Whitespace" "The greater-than operator should not alter surrounding whitespace.")) (is (mustache:render* "{{>node}}" (mustache:make-context :data '((:content . "X") (:nodes . #(((:content . "Y") (:nodes . #()))))) :partials '((:node . "{{content}}<{{#nodes}}{{>node}}{{/nodes}}>")))) "X>" (format nil "~A :: ~A" "Recursion" "The greater-than operator should properly recurse.")) (is (mustache:render* "\"{{>partial}}\"" (mustache:make-context :data '((:text . "content")) :partials '((:partial . "*{{text}}*")))) "\"*content*\"" (format nil "~A :: ~A" "Context" "The greater-than operator should operate within the current context.")) (is (mustache:render* "\"{{>text}}\"" (mustache:make-context :data 'nil :partials 'nil)) "\"\"" (format nil "~A :: ~A" "Failed Lookup" "The empty string should be used when the named partial is not found.")) (is (mustache:render* "\"{{>text}}\"" (mustache:make-context :data 'nil :partials '((:text . "from partial")))) "\"from partial\"" (format nil "~A :: ~A" "Basic Behavior" "The greater-than operator should expand to the named partial.")) (is (mustache:render* "|{{# boolean }}={{/ boolean }}|" (mustache:make-context :data '((:boolean . t)) :partials 'nil)) "|=|" (format nil "~A :: ~A" "Padding" "Superfluous in-tag whitespace should be ignored.")) (is (mustache:render* "#{{#boolean}} / {{/boolean}}" (mustache:make-context :data '((:boolean . t)) :partials 'nil)) "# / " (format nil "~A :: ~A" "Standalone Without Newline" "Standalone tags should not require a newline to follow them.")) (is (mustache:render* " {{#boolean}} #{{/boolean}} /" (mustache:make-context :data '((:boolean . t)) :partials 'nil)) "# /" (format nil "~A :: ~A" "Standalone Without Previous Line" "Standalone tags should not require a newline to precede them.")) (is (mustache:render* "| {{#boolean}} {{/boolean}} |" (mustache:make-context :data '((:boolean . t)) :partials 'nil)) "| |" (format nil "~A :: ~A" "Standalone Line Endings" "\"\\r\\n\" should be considered a newline for standalone tags.")) (is (mustache:render* "| This Is {{#boolean}} | {{/boolean}} | A Line " (mustache:make-context :data '((:boolean . t)) :partials 'nil)) "| This Is | | A Line " (format nil "~A :: ~A" "Indented Standalone Lines" "Indented standalone lines should be removed from the template.")) (is (mustache:render* "| This Is {{#boolean}} | {{/boolean}} | A Line " (mustache:make-context :data '((:boolean . t)) :partials 'nil)) "| This Is | | A Line " (format nil "~A :: ~A" "Standalone Lines" "Standalone lines should be removed from the template.")) (is (mustache:render* " {{#boolean}}YES{{/boolean}} {{#boolean}}GOOD{{/boolean}} " (mustache:make-context :data '((:boolean . t)) :partials 'nil)) " YES GOOD " (format nil "~A :: ~A" "Indented Inline Sections" "Single-line sections should not alter surrounding whitespace.")) (is (mustache:render* " | {{#boolean}} {{! Important Whitespace }} {{/boolean}} | " (mustache:make-context :data '((:boolean . t)) :partials 'nil)) " | | " (format nil "~A :: ~A" "Internal Whitespace" "Sections should not alter internal whitespace.")) (is (mustache:render* " | {{#boolean}} | {{/boolean}} | " (mustache:make-context :data '((:boolean . t)) :partials 'nil)) " | | | " (format nil "~A :: ~A" "Surrounding Whitespace" "Sections should not alter surrounding whitespace.")) (is (mustache:render* "\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"" (mustache:make-context :data '((:a)) :partials 'nil)) "\"\" == \"\"" (format nil "~A :: ~A" "Dotted Names - Broken Chains" "Dotted names that cannot be resolved should be considered falsey.")) (is (mustache:render* "\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"" (mustache:make-context :data '((:a (:b (:c)))) :partials 'nil)) "\"\" == \"\"" (format nil "~A :: ~A" "Dotted Names - Falsey" "Dotted names should be valid for Section tags.")) (is (mustache:render* "\"{{#a.b.c}}Here{{/a.b.c}}\" == \"Here\"" (mustache:make-context :data '((:a (:b (:c . t)))) :partials 'nil)) "\"Here\" == \"Here\"" (format nil "~A :: ~A" "Dotted Names - Truthy" "Dotted names should be valid for Section tags.")) (is (with-standard-io-syntax (mustache:render* "\"{{#list}}({{.}}){{/list}}\"" (mustache:make-context :data '((:list . #(1.1f0 2.2f0 3.3f0 4.4f0 5.5f0))) :partials 'nil))) "\"(1.1)(2.2)(3.3)(4.4)(5.5)\"" (format nil "~A :: ~A" "Implicit Iterator - Decimal" "Implicit iterators should cast decimals to strings and interpolate.")) (is (mustache:render* "\"{{#list}}({{.}}){{/list}}\"" (mustache:make-context :data '((:list . #(1 2 3 4 5))) :partials 'nil)) "\"(1)(2)(3)(4)(5)\"" (format nil "~A :: ~A" "Implicit Iterator - Integer" "Implicit iterators should cast integers to strings and interpolate.")) (is (mustache:render* "\"{{#list}}({{.}}){{/list}}\"" (mustache:make-context :data '((:list . #("a" "b" "c" "d" "e"))) :partials 'nil)) "\"(a)(b)(c)(d)(e)\"" (format nil "~A :: ~A" "Implicit Iterator - String" "Implicit iterators should directly interpolate strings.")) (is (mustache:render* "\"{{#list}}({{.}}){{/list}}\"" (mustache:make-context :data '((:list . #("" "" "" "" ""))) :partials 'nil)) "\"(<a>)(<b>)(<c>)(<d>)(<e>)\"" (format nil "~A :: ~A" "Implicit Iterator - String" "Implicit iterators should be properly escaped.")) (is (mustache:render* "\"{{#list}}({{{.}}}){{/list}}\"" (mustache:make-context :data '((:list . #("" "" "" "" ""))) :partials 'nil)) "\"()()()()()\"" (format nil "~A :: ~A" "Implicit Iterator - String" "Triple Mustache implicit iterators should interpolate without HTML escaping.")) (is (mustache:render* "[{{#missing}}Found key 'missing'!{{/missing}}]" (mustache:make-context :data 'nil :partials 'nil)) "[]" (format nil "~A :: ~A" "Context Misses" "Failed context lookups should be considered falsey.")) (is (mustache:render* "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |" (mustache:make-context :data '((:bool)) :partials 'nil)) "| A E |" (format nil "~A :: ~A" "Nested (Falsey)" "Nested falsey sections should be omitted.")) (is (mustache:render* "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |" (mustache:make-context :data '((:bool . t)) :partials 'nil)) "| A B C D E |" (format nil "~A :: ~A" "Nested (Truthy)" "Nested truthy sections should have their contents rendered.")) (is (mustache:render* "{{#bool}} * first {{/bool}} * {{two}} {{#bool}} * third {{/bool}} " (mustache:make-context :data '((:two . "second") (:bool . t)) :partials 'nil)) "* first * second * third " (format nil "~A :: ~A" "Doubled" "Multiple sections per template should be permitted.")) (is (mustache:render* "\"{{#list}}Yay lists!{{/list}}\"" (mustache:make-context :data '((:list . #())) :partials 'nil)) "\"\"" (format nil "~A :: ~A" "Empty List" "Empty lists should behave like falsey values.")) (is (mustache:render* "\"{{#list}}{{item}}{{/list}}\"" (mustache:make-context :data '((:list . #(((:item . 1)) ((:item . 2)) ((:item . 3))))) :partials 'nil)) "\"123\"" (format nil "~A :: ~A" "List" "Lists should be iterated; list items should visit the context stack.")) (is (mustache:render* "{{#a}} {{one}} {{#b}} {{one}}{{two}}{{one}} {{#c}} {{one}}{{two}}{{three}}{{two}}{{one}} {{#d}} {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}} {{#e}} {{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}} {{/e}} {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}} {{/d}} {{one}}{{two}}{{three}}{{two}}{{one}} {{/c}} {{one}}{{two}}{{one}} {{/b}} {{one}} {{/a}} " (mustache:make-context :data '((:a (:one . 1)) (:b (:two . 2)) (:c (:three . 3)) (:d (:four . 4)) (:e (:five . 5))) :partials 'nil)) "1 121 12321 1234321 123454321 1234321 12321 121 1 " (format nil "~A :: ~A" "Deeply Nested Contexts" "All elements on the context stack should be accessible.")) (is (mustache:render* "\"{{#context}}Hi {{name}}.{{/context}}\"" (mustache:make-context :data '((:context (:name . "Joe"))) :partials 'nil)) "\"Hi Joe.\"" (format nil "~A :: ~A" "Context" "Objects and hashes should be pushed onto the context stack.")) (is (mustache:render* "\"{{#boolean}}This should not be rendered.{{/boolean}}\"" (mustache:make-context :data '((:boolean)) :partials 'nil)) "\"\"" (format nil "~A :: ~A" "Falsey" "Falsey sections should have their contents omitted.")) (is (mustache:render* "\"{{#boolean}}This should be rendered.{{/boolean}}\"" (mustache:make-context :data '((:boolean . t)) :partials 'nil)) "\"This should be rendered.\"" (format nil "~A :: ~A" "Truthy" "Truthy sections should have their contents rendered.")) (is (mustache:render* "<{{^lambda}}{{static}}{{/lambda}}>" (mustache:make-context :data `((:static . "static") (:lambda . ,(lambda (text) (declare (ignore text))))) :partials 'nil)) "<>" (format nil "~A :: ~A" "Inverted Section" "Lambdas used for inverted sections should be considered truthy.")) (is (mustache:render* "{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}" (mustache:make-context :data `((:lambda . ,(lambda (text) (format nil "__~a__" text)))) :partials 'nil)) "__FILE__ != __LINE__" (format nil "~A :: ~A" "Section - Multiple Calls" "Lambdas used for sections should not be cached.")) (is (mustache:render* "{{= | | =}}<|#lambda|-|/lambda|>" (mustache:make-context :data `((:planet . "Earth") (:lambda . ,(lambda (text) (format nil "~a{{planet}} => |planet|~a" text text)))) :partials 'nil)) "<-{{planet}} => Earth->" (format nil "~A :: ~A" "Section - Alternate Delimiters" "Lambdas used for sections should parse with the current delimiters.")) (is (mustache:render* "<{{#lambda}}-{{/lambda}}>" (mustache:make-context :data `((:planet . "Earth") (:lambda . ,(lambda (text) (format nil "~a{{planet}}~a" text text)))) :partials 'nil)) "<-Earth->" (format nil "~A :: ~A" "Section - Expansion" "Lambdas used for sections should have their results parsed.")) (is (mustache:render* "<{{#lambda}}{{x}}{{/lambda}}>" (mustache:make-context :data `((:x . "Error!") (:lambda . ,(lambda (text) (if (equal text "{{x}}") "yes" "no")))) :partials 'nil)) "" (format nil "~A :: ~A" "Section" "Lambdas used for sections should receive the raw section string.")) (is (mustache:render* "<{{lambda}}{{{lambda}}}" (mustache:make-context :data `((:lambda . ,(lambda () ">"))) :partials 'nil)) "<>>" (format nil "~A :: ~A" "Escaping" "Lambda results should be appropriately escaped.")) (is (mustache:render* "{{lambda}} == {{{lambda}}} == {{lambda}}" (mustache:make-context :data `((:lambda . ,(let ((calls 0)) (lambda () (incf calls))))) :partials 'nil)) "1 == 2 == 3" (format nil "~A :: ~A" "Interpolation - Multiple Calls" "Interpolated lambdas should not be cached.")) (is (mustache:render* "{{= | | =}} Hello, (|&lambda|)!" (mustache:make-context :data `((:planet . "world") (:lambda . ,(lambda () "|planet| => {{planet}}"))) :partials 'nil)) "Hello, (|planet| => world)!" (format nil "~A :: ~A" "Interpolation - Alternate Delimiters" "A lambda's return value should parse with the default delimiters.")) (is (mustache:render* "Hello, {{lambda}}!" (mustache:make-context :data `((:planet . "world") (:lambda . ,(lambda () "{{planet}}"))) :partials 'nil)) "Hello, world!" (format nil "~A :: ~A" "Interpolation - Expansion" "A lambda's return value should be parsed.")) (is (mustache:render* "Hello, {{lambda}}!" (mustache:make-context :data `((:lambda . ,(lambda () "world"))) :partials 'nil)) "Hello, world!" (format nil "~A :: ~A" "Interpolation" "A lambda's return value should be interpolated.")) (finalize) cl-mustache/t/test.mustache000066400000000000000000000000231257077450200162440ustar00rootroot00000000000000TEST {{test}} TEST cl-mustache/version.lisp-expr000066400000000000000000000000311257077450200166200ustar00rootroot00000000000000;; -*- lisp -*- "0.12.3"