├── .gitignore ├── src └── org │ └── panchromatic │ ├── mokuhan │ ├── util │ │ ├── misc.cljc │ │ ├── regex.cljc │ │ └── stringbuilder.cljc │ ├── walker │ │ ├── protocol.cljc │ │ ├── platform.clj │ │ └── platform.cljs │ ├── walker.cljc │ ├── renderer │ │ ├── platform.clj │ │ ├── protocol.cljc │ │ └── platform.cljs │ ├── renderer.cljc │ ├── ast.cljc │ └── parser.cljc │ └── mokuhan.cljc ├── test └── org │ └── panchromatic │ ├── mokuhan │ ├── test_runner.cljs │ ├── test_helper.cljc │ ├── walker_test.cljc │ ├── renderer_test.cljc │ └── parser_test.clj │ └── mokuhan_test.cljc ├── .circleci └── config.yml ├── CHANGELOG.md ├── LICENSE ├── README.org └── project.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .cljs_rhino_repl 13 | .cljs_node_repl 14 | out 15 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/util/misc.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.util.misc) 2 | 3 | (defn truthy? [o] 4 | (and (not (false? o)) (some? o))) 5 | 6 | (defn meta-without-qualifiers [x] 7 | (some->> (meta x) 8 | (reduce-kv #(assoc %1 (-> %2 name keyword) %3) {}))) 9 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/util/regex.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.util.regex 2 | #?(:clj (:import java.util.regex.Pattern))) 3 | 4 | (defn re-quote [s] 5 | #?(:clj 6 | (re-pattern (Pattern/quote (str s))) 7 | :cljs 8 | (re-pattern (.replace s (js/RegExp. "\\W" "g") #(str "\\" %))))) 9 | 10 | (defn source [regex] 11 | #?(:clj 12 | (.toString regex) 13 | :cljs 14 | (.-source regex))) 15 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan 2 | (:require [org.panchromatic.mokuhan.parser :as parser] 3 | [org.panchromatic.mokuhan.renderer :as renderer])) 4 | 5 | (defn render 6 | ([mustache data] 7 | (render mustache data {})) 8 | 9 | ([mustache data opts] 10 | (let [render' (fn [& [opts']] #(render % data (merge opts opts')))] 11 | (-> (parser/parse mustache opts) 12 | (renderer/render data (assoc opts :render render')))))) 13 | -------------------------------------------------------------------------------- /test/org/panchromatic/mokuhan/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.test-runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | org.panchromatic.mokuhan-test 4 | org.panchromatic.mokuhan.renderer-test 5 | org.panchromatic.mokuhan.walker-test)) 6 | 7 | (enable-console-print!) 8 | 9 | (doo-tests 'org.panchromatic.mokuhan.walker-test 10 | 'org.panchromatic.mokuhan.renderer-test 11 | 'org.panchromatic.mokuhan-test) 12 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/walker/protocol.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.walker.protocol) 2 | 3 | (defrecord FoundKey [value]) 4 | 5 | (defn found-key 6 | ([] (FoundKey. nil)) 7 | ([value] (FoundKey. value))) 8 | 9 | (defn found-key? [x] 10 | (instance? FoundKey x)) 11 | 12 | (defprotocol Traverser 13 | (traverse [this path])) 14 | 15 | (extend-protocol Traverser 16 | nil 17 | (traverse [_ _]) 18 | 19 | FoundKey 20 | (traverse [fk path] 21 | (traverse 22 | #?(:clj (.value fk) :cljs (.-value fk)) 23 | path))) 24 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/util/stringbuilder.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.util.stringbuilder 2 | #?(:cljs (:require [clojure.string :as str]))) 3 | 4 | #?(:clj 5 | (defn new-string-builder [] 6 | (StringBuilder.)) 7 | 8 | :cljs 9 | (defn new-string-builder [] 10 | (atom []))) 11 | 12 | #?(:clj 13 | (defn append [^StringBuilder sb ^String s] 14 | (.append sb s)) 15 | 16 | :cljs 17 | (defn append [sb s] 18 | (swap! sb conj s) 19 | sb)) 20 | 21 | #?(:clj 22 | (defn to-string [^StringBuilder sb] 23 | (.toString sb)) 24 | 25 | :cljs 26 | (defn to-string [sb] 27 | (str/join @sb))) 28 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: jesiio/web:0.1 6 | 7 | working_directory: ~/repo 8 | 9 | environment: 10 | LEIN_ROOT: "true" 11 | JVM_OPTS: -Xmx3200m 12 | 13 | steps: 14 | - checkout 15 | 16 | - restore_cache: 17 | keys: 18 | - v1-dependencies-{{ checksum "project.clj" }} 19 | - v1-dependencies- 20 | 21 | - run: lein with-profiles +cljstest deps 22 | 23 | - save_cache: 24 | paths: 25 | - ~/.m2 26 | key: v1-dependencies-{{ checksum "project.clj" }} 27 | 28 | - run: lein test-all 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2018-03-04 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2018-03-04 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://github.com/your-name/mustaclj/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/your-name/mustaclj/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /test/org/panchromatic/mokuhan/test_helper.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.test-helper 2 | #?(:clj (:require [cheshire.core :as c] 3 | [clj-http.client :as http] 4 | [clojure.walk :as walk]))) 5 | 6 | #?(:clj 7 | (defn- get-spec [url] 8 | (c/decode (:body (http/get url)) true))) 9 | 10 | #?(:clj 11 | (defmacro generate-test-cases-from-spec [which-spec] 12 | (let [url (str "https://raw.githubusercontent.com/mustache/spec/master/specs/" which-spec ".json") 13 | spec (get-spec url)] 14 | `(t/deftest ~(symbol (str which-spec "-test")) 15 | ~@(for [test (:tests spec)] 16 | `(t/testing ~(str (:name test) " / " (:desc test)) 17 | (t/is (= ~(:expected test) 18 | (sut/render ~(:template test) ~(:data test) ~(select-keys test [:partials])))))))))) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ayato-p 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 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Mokuhan (木版) [[https://circleci.com/gh/ayato-p/mokuhan/tree/master][https://circleci.com/gh/ayato-p/mokuhan/tree/master.svg?style=svg]] [[https://opensource.org/licenses/MIT][https://img.shields.io/badge/License-MIT-blue.svg]] 2 | 3 | Mokuhan is yet another implementation of Mustache in Clojure/ClojureScript. 4 | 5 | ** Installation 6 | 7 | Add the following dependency to your =project.clj= or =deps.edn= file: 8 | 9 | [[https://clojars.org/org.panchromatic/mokuhan][https://img.shields.io/clojars/v/org.panchromatic/mokuhan.svg]] 10 | 11 | ** License 12 | 13 | MIT License 14 | 15 | Copyright (c) 2018 ayato-p 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy 18 | of this software and associated documentation files (the "Software"), to deal 19 | in the Software without restriction, including without limitation the rights 20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | copies of the Software, and to permit persons to whom the Software is 22 | furnished to do so, subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in all 25 | copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | SOFTWARE. 34 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/walker.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.walker 2 | (:require [clojure.math.combinatorics :as comb] 3 | [org.panchromatic.mokuhan.walker.platform :as platform] 4 | [org.panchromatic.mokuhan.walker.protocol :as proto])) 5 | 6 | ;; don't remove platform ns via ns clean-up 7 | ::platform/require 8 | 9 | (defn- path-candidates [path] 10 | (if (>= 1 (count path)) 11 | (list path) 12 | (let [v (peek path) 13 | path (map-indexed #(vector (inc %1) %2) (pop path))] 14 | (concat 15 | (map 16 | #(conj (nth % 1) v) 17 | (sort-by 18 | #(+ (nth % 0) (count (nth % 1))) 19 | > 20 | (reduce (fn [v i] 21 | (->> (comb/combinations path i) 22 | (map #(reduce (fn [w [j x]] 23 | (-> w (update 0 * j) (update 1 conj x))) 24 | [1 []] 25 | (sort-by first < %))) 26 | (concat v))) 27 | () 28 | (range 1 (inc (count path)))))) 29 | (list [v]))))) 30 | 31 | (defn traverse* [x paths] 32 | (loop [[path & candidates] (path-candidates paths)] 33 | (when path 34 | (if-let [x (reduce #(cond-> %1 35 | (not= (first %2) ".") 36 | (proto/traverse %2)) 37 | x 38 | path)] 39 | (cond-> x (proto/found-key? x) #?(:clj (.value) :cljs (.-value))) 40 | (recur candidates))))) 41 | 42 | (defn traverse 43 | ([x path position] 44 | (->> (conj (vec position) path) 45 | (traverse* x)))) 46 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/renderer/platform.clj: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.renderer.platform 2 | (:require [org.panchromatic.mokuhan.ast :as ast] 3 | [org.panchromatic.mokuhan.renderer.protocol :as proto] 4 | [org.panchromatic.mokuhan.util.stringbuilder :as sb])) 5 | 6 | (extend-type clojure.lang.AFunction 7 | proto/Renderable 8 | (render [f context state] 9 | (let [render ((:render state identity))] 10 | (-> (f) (proto/render context state) render))) 11 | 12 | proto/StandardSectionRenderer 13 | (render-section [f section context state] 14 | ;; Temporary fix 15 | (let [delimiters (ast/get-open-tag-delimiters section) 16 | render ((:render state) {:delimiters delimiters})] 17 | (-> (ast/children section) 18 | (->> (reduce #(sb/append %1 (.toString %2)) (sb/new-string-builder))) 19 | sb/to-string 20 | f 21 | (proto/render context state) 22 | render)))) 23 | 24 | (extend-type java.util.List 25 | proto/StandardSectionRenderer 26 | (render-section [l section context state] 27 | (let [path (.path section) 28 | contents (.contents section)] 29 | (->> (for [idx (range (count l)) ast contents] [idx ast]) 30 | (reduce (fn [sb [idx ast]] 31 | (->> (-> state 32 | (update :position (fnil conj []) path [idx])) 33 | (proto/render ast context) 34 | (.append sb))) 35 | (StringBuilder. 1024)) 36 | (.toString)))) 37 | 38 | proto/InvertedSectionRenderer 39 | (render-inverted-section [l section context state] 40 | (if (.isEmpty l) 41 | (proto/render-section-simply section context state) 42 | ""))) 43 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/renderer/protocol.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.renderer.protocol 2 | (:require [org.panchromatic.mokuhan.util.misc :as misc] 3 | [org.panchromatic.mokuhan.util.stringbuilder :as sb])) 4 | 5 | (defprotocol Renderable 6 | (render [this context state])) 7 | 8 | (extend-protocol Renderable 9 | nil 10 | (render 11 | ([_ _ _] "")) 12 | 13 | #?(:clj Object :cljs default) 14 | (render 15 | ([o _ _] (.toString o)))) 16 | 17 | (defn render-section-simply [section context state] 18 | (let [path #?(:clj (.path section) :cljs (:path section)) 19 | contents #?(:clj (.contents section) :cljs (:contents section))] 20 | (->> contents 21 | (reduce (fn [sb ast] 22 | (->> (update state :position conj path) 23 | (render ast context) 24 | (sb/append sb))) 25 | (sb/new-string-builder)) 26 | (sb/to-string)))) 27 | 28 | 29 | (defprotocol StandardSectionRenderer 30 | (render-section [this section context state])) 31 | 32 | (extend-protocol StandardSectionRenderer 33 | nil 34 | (render-section [_ _ _ _] 35 | "") 36 | 37 | #?(:clj Object :cljs default) 38 | (render-section [o section context state] 39 | (if (misc/truthy? o) 40 | (render-section-simply section context state) 41 | ""))) 42 | 43 | 44 | (defprotocol InvertedSectionRenderer 45 | (render-inverted-section [this section context state])) 46 | 47 | (extend-protocol InvertedSectionRenderer 48 | nil 49 | (render-inverted-section [_ section context state] 50 | (render-section-simply section context state)) 51 | 52 | #?(:clj Object :cljs default) 53 | (render-inverted-section [o section context state] 54 | (if-not (misc/truthy? o) 55 | (render-section-simply section context state) 56 | ""))) 57 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.panchromatic/mokuhan "0.1.2-SNAPSHOT" 2 | :description "Yet another implementation of Mustache in Clojure." 3 | :url "https://github.com/ayato-p/mokuhan" 4 | :license {:name "MIT License" 5 | :url "https://choosealicense.com/licenses/mit"} 6 | 7 | :deploy-repositories [["releases" :clojars]] 8 | 9 | :dependencies [[fast-zip "0.7.0"] 10 | [instaparse "1.4.8"] 11 | [org.clojure/math.combinatorics "0.1.4"]] 12 | 13 | :profiles 14 | {:provided 15 | {:dependencies [[org.clojure/clojure "1.9.0"] 16 | [org.clojure/clojurescript "1.9.946"]]} 17 | 18 | :dev 19 | {:dependencies [[cheshire "5.8.0"] 20 | [clj-http "3.8.0"] 21 | [com.cemerick/piggieback "0.2.2"] 22 | [doo "0.1.10"]]} 23 | 24 | :cljstest 25 | [:plugin/cljsbuild :plugin/doo] 26 | 27 | :plugin/cljsbuild 28 | {:plugins [[lein-cljsbuild "1.1.7"]] 29 | :cljsbuild 30 | {:builds 31 | {:node-test {:source-paths ["src" "test"] 32 | :compiler {:output-to "target/node-test/org/panchromatic/mokuhan-test.js" 33 | :output-dir "target/node-test/org/panchromatic" 34 | :main org.panchromatic.mokuhan.test-runner 35 | :optimizations :advanced 36 | :target :nodejs}} 37 | 38 | :phantom-test {:source-paths ["src" "test"] 39 | :compiler {:output-to "target/phantom-test/org/panchromatic/mokuhan-test.js" 40 | :output-dir "target/phantom-test/org/panchromatic" 41 | :main org.panchromatic.mokuhan.test-runner 42 | :optimizations :advanced}}}}} 43 | 44 | :plugin/doo 45 | {:plugins [[lein-doo "0.1.10"]]}} 46 | 47 | :aliases 48 | {"cljstest-node" ["doo" "node" "node-test" "once"] 49 | "cljstest-phantom" ["doo" "phantom" "phantom-test" "once"] 50 | "cljstest" ["with-profile" "+cljstest" "do" ["cljstest-node"] ["cljstest-phantom"]] 51 | "test-all" ["do" ["cljstest"] ["test"]]}) 52 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/walker/platform.clj: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.walker.platform 2 | (:require [clojure.reflect :as reflect] 3 | [org.panchromatic.mokuhan.walker.protocol :as proto])) 4 | 5 | (defn- invokable-members [o] 6 | (let [{:keys [bases members]} (reflect/reflect o) 7 | xform (comp (filter #(empty? (:parameter-types %))) 8 | (map :name) 9 | (map str))] 10 | (->> (map resolve bases) 11 | (reduce #(into %1 (:members (reflect/reflect %2))) members) 12 | (into #{} xform)))) 13 | 14 | (defn- invoke-instance-member [^Object o ^String method-name] 15 | (try 16 | (clojure.lang.Reflector/invokeInstanceMember o method-name) 17 | (catch Exception e))) 18 | 19 | (extend-protocol proto/Traverser 20 | Object 21 | (traverse 22 | ([o path] 23 | (let [[p & path] path] 24 | (-> (if (= p ".") 25 | o 26 | (when ((invokable-members o) p) 27 | (let [x (invoke-instance-member o p)] 28 | (or x (proto/found-key x))))) 29 | (cond-> (seq path) (proto/traverse path)))))) 30 | 31 | java.util.Map 32 | (traverse 33 | ([^java.util.Map m path] 34 | (let [[p & path] path 35 | p (cond-> p (not (string? p)) str)] 36 | (-> (if (= p ".") 37 | m 38 | (reduce (fn [_ k] 39 | (when (.containsKey m k) 40 | (reduced 41 | (let [x (.get m k)] 42 | (or x (proto/found-key x)))))) 43 | nil 44 | [p (keyword p) (symbol p)])) 45 | (cond-> (seq path) (proto/traverse path)))))) 46 | 47 | java.util.List 48 | (traverse 49 | ([^java.util.List l path] 50 | (let [[p & path] path] 51 | (-> (if (= p ".") 52 | l 53 | (let [p (if (integer? p) 54 | p 55 | (some->> p str (re-matches #"\d+") (Long/parseLong))) 56 | size (.size l)] 57 | (when (and p (<= p (dec size))) 58 | (let [x (.get l p)] 59 | (or x (proto/found-key x)))))) 60 | (cond-> (seq path) (proto/traverse path))))))) 61 | -------------------------------------------------------------------------------- /test/org/panchromatic/mokuhan/walker_test.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.walker-test 2 | (:require [org.panchromatic.mokuhan.walker :as sut] 3 | [clojure.test :as t])) 4 | 5 | (t/deftest traverse-test 6 | (t/testing "simple" 7 | (t/are [x path position expected] (= expected (sut/traverse x path position)) 8 | {:x 42} ["x"] [] 42 9 | {:x 42} ["."] [["x"]] 42)) 10 | 11 | (t/testing "nested map" 12 | (t/are [x path position expected] (= expected (sut/traverse x path position)) 13 | {:x {:y 42}} ["y"] [["x"]] 42 14 | {:x {:y 42}} ["x" "y"] [] 42 15 | {:x {:y 42}} ["."] [["x" "y"]] 42 16 | {:x {:y 42}} ["."] [["x"] ["y"]] 42 17 | {:x {:y 42}} ["."] [["x"] ["."] ["y"]] 42 18 | {:x {:y 42}} ["."] [["x"] ["."] ["y"] ["."]] 42)) 19 | 20 | (t/testing "found a key in a nested map" 21 | (t/are [x path position expected] (= expected (sut/traverse x path position)) 22 | {:x {:y nil} :y 42} ["y"] [["x"]] nil 23 | {:x {:y nil} :y 42} ["x" "y"] [] nil 24 | {:x {:y nil} :y 42} ["."] [["x" "y"]] nil)) 25 | 26 | (t/testing "traverser could not find a key in a nested map" 27 | (t/are [x path position expected] (= expected (sut/traverse x path position)) 28 | {:x {} :y 42} ["y"] [["x"]] 42 29 | {:x {} :y 42} ["y"] [["x"] ["."]] 42 30 | {:x {} :y 42} ["."] [["x"] ["y"]] 42)) 31 | 32 | (t/testing "with list" 33 | (t/are [x path position expected] (= expected (sut/traverse x path position)) 34 | {:x [{:y 41} {:y 42} {:y 43}]} ["y"] [["x"] [1]] 42 35 | {:x [{:y 41} {:y 42} {:y 43}]} ["y"] [["x"] ["."] [1]] 42 36 | {:x [{:y 41} {:y 42} {:y 43}]} ["y"] [["x"] ["."] [1] ["."]] 42 37 | {:x [{:y 41} {:y 42} {:y 43}]} ["."] [["x"] ["."] [1] ["y"]] 42)) 38 | 39 | (t/testing "with list and could not find a key in a nested map" 40 | (t/are [x path position expected] (= expected (sut/traverse x path position)) 41 | {:x [{:z 1} {:z 2}] :y 42} ["y"] [["x"]] 42 42 | {:x [{:z 1} {:z 2}] :y 42} ["y"] [["x"] [0]] 42 43 | {:x [{:z 1} {:z 2}] :y 42} ["y"] [["x"] ["0"]] 42 44 | {:x [{:z 1} {:z 2}] :y 42} ["y"] [["x"] ["."] [0]] 42 45 | {:x [{:z 1} {:z 2}] :y 42} ["y"] [["x"] [0] ["."]] 42 46 | {:x [{:z 1} {:z 2}] :y 42} ["."] [["x"] [0] ["y"]] 42))) 47 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/renderer.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.renderer 2 | (:require [clojure.string :as str] 3 | [org.panchromatic.mokuhan.ast :as ast 4 | #?@(:cljs [:refer [EscapedVariable InvertedSection Mustache StandardSection UnescapedVariable]])] 5 | [org.panchromatic.mokuhan.renderer.platform :as platform] 6 | [org.panchromatic.mokuhan.renderer.protocol :as proto] 7 | [org.panchromatic.mokuhan.util.stringbuilder :as sb] 8 | [org.panchromatic.mokuhan.walker :as walker]) 9 | #?(:clj 10 | (:import [org.panchromatic.mokuhan.ast EscapedVariable InvertedSection Mustache StandardSection UnescapedVariable]))) 11 | 12 | ;; don't remove platform ns via ns clean-up 13 | ::platform/require 14 | 15 | (defn- path [x] 16 | #?(:clj (.path x) 17 | :cljs (.-path x))) 18 | 19 | (defn- escape-html [s] 20 | (-> s 21 | (str/replace #"&" "&") 22 | (str/replace #"<" "<") 23 | (str/replace #">" ">") 24 | (str/replace #"\"" """) 25 | (str/replace #"'" "'"))) 26 | 27 | (extend-protocol proto/Renderable 28 | Mustache 29 | (render 30 | ([mustache context state] 31 | (-> (reduce (fn [sb c] 32 | (->> (proto/render c context state) 33 | (sb/append sb))) 34 | (sb/new-string-builder) 35 | (ast/children mustache)) 36 | (sb/to-string)))) 37 | 38 | EscapedVariable 39 | (render 40 | ([variable context state] 41 | (let [position (:position state)] 42 | (-> (walker/traverse context (path variable) position) 43 | (proto/render context state) 44 | escape-html)))) 45 | 46 | UnescapedVariable 47 | (render 48 | ([variable context state] 49 | (let [position (:position state)] 50 | (-> (walker/traverse context (path variable) position) 51 | (proto/render context state))))) 52 | 53 | StandardSection 54 | (render [section context state] 55 | (-> (walker/traverse context (path section) (:position state)) 56 | (proto/render-section section context state))) 57 | 58 | InvertedSection 59 | (render [section context state] 60 | (-> (walker/traverse context (path section) (:position state)) 61 | (proto/render-inverted-section section context state)))) 62 | 63 | (def ^:private initial-state 64 | {:position [] 65 | :render (constantly identity)}) 66 | 67 | (defn render 68 | ([ast context] 69 | (render ast context initial-state)) 70 | ([ast context state] 71 | (->> (merge initial-state state) 72 | (proto/render ast context)))) 73 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/walker/platform.cljs: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.walker.platform 2 | (:require [goog.array :as ary] 3 | [goog.object :as o] 4 | [org.panchromatic.mokuhan.walker.protocol :as proto])) 5 | 6 | (defn- traverse-map [m [p & path]] 7 | (let [p (cond-> p (not (string? p)) str)] 8 | (-> (if (= p ".") 9 | m 10 | (reduce (fn [_ k] 11 | (when (contains? m k) 12 | (reduced 13 | (let [x (get m k)] 14 | (or x (proto/found-key x)))))) 15 | nil 16 | [p (keyword p) (symbol p)])) 17 | (cond-> (seq path) (proto/traverse path))))) 18 | 19 | (extend-protocol proto/Traverser 20 | default 21 | (traverse [_ _] nil) 22 | 23 | object 24 | (traverse [o path] 25 | (let [[p & path] path] 26 | (-> (if (= p ".") 27 | o 28 | (when (o/containsKey o p) 29 | (let [x (o/get o p)] 30 | (or (cond-> x (fn? x) (.call)) 31 | (proto/found-key x))))) 32 | (cond-> (seq path) (proto/traverse path))))) 33 | 34 | array 35 | (traverse [ary path] 36 | (let [[p & path] path] 37 | (-> (if (= p ".") 38 | ary 39 | (let [p (if (integer? p) 40 | p 41 | (some->> p str (re-matches #"\d+") (js/parseInt))) 42 | size (.-length ary)] 43 | (when (and p (<= p (dec size))) 44 | (let [x (aget ary p)] 45 | (or x (proto/found-key x)))))) 46 | (cond-> (seq path) (proto/traverse path))))) 47 | 48 | cljs.core/EmptyList 49 | (traverse [l path] 50 | (proto/traverse (into-array l) path)) 51 | 52 | cljs.core/List 53 | (traverse [l path] 54 | (proto/traverse (into-array l) path)) 55 | 56 | cljs.core/PersistentVector 57 | (traverse [v path] 58 | (let [[p & path] path] 59 | (-> (if (= p ".") 60 | v 61 | (let [p (if (integer? p) 62 | p 63 | (some->> p str (re-matches #"\d+") (js/parseInt))) 64 | size (count v)] 65 | (when (and p (<= p (dec size))) 66 | (let [x (get v p)] 67 | (or x (proto/found-key x)))))) 68 | (cond-> (seq path) (proto/traverse path))))) 69 | 70 | 71 | cljs.core/PersistentHashMap 72 | (traverse [m path] 73 | (traverse-map m path)) 74 | 75 | cljs.core/PersistentTreeMap 76 | (traverse [m path] 77 | (traverse-map m path)) 78 | 79 | cljs.core/PersistentArrayMap 80 | (traverse [m path] 81 | (traverse-map m path))) 82 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/renderer/platform.cljs: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.renderer.platform 2 | (:require [org.panchromatic.mokuhan.ast :as ast :refer [StandardSection]] 3 | [org.panchromatic.mokuhan.renderer.protocol :as proto] 4 | [org.panchromatic.mokuhan.util.stringbuilder :as sb])) 5 | 6 | (extend-type function 7 | proto/Renderable 8 | (render [f data state] 9 | (let [render ((:render state identity))] 10 | (-> (f) (proto/render data state) render))) 11 | 12 | proto/StandardSectionRenderer 13 | (render-section [f section data state] 14 | (let [delimiters (ast/get-open-tag-delimiters section) 15 | render ((:render state identity) {:delimiters delimiters})] 16 | (-> (ast/children section) 17 | (->> (reduce #(sb/append %1 (.toString %2)) (sb/new-string-builder))) 18 | sb/to-string 19 | f 20 | (proto/render data state) 21 | render)))) 22 | 23 | (extend-type array 24 | proto/StandardSectionRenderer 25 | (render-section [ary ^StandardSection section data state] 26 | (let [path (.-path section) 27 | contents (.-contents section)] 28 | (->> (for [idx (range (.-length ary)) ast contents] [idx ast]) 29 | (reduce (fn [sb [idx ast]] 30 | (->> (-> state 31 | (update :position (fnil conj []) path [idx])) 32 | (proto/render ast data) 33 | (sb/append sb))) 34 | (sb/new-string-builder)) 35 | (sb/to-string)))) 36 | 37 | proto/InvertedSectionRenderer 38 | (render-inverted-section [ary section data state] 39 | (proto/render-section-simply section data state))) 40 | 41 | (defn- render-section-seq [l-or-v ^StandardSection section data state] 42 | (let [path (.-path section) 43 | contents (.-contents section)] 44 | (->> (for [idx (range (count l-or-v)) ast contents] [idx ast]) 45 | (reduce (fn [sb [idx ast]] 46 | (->> (-> state 47 | (update :position (fnil conj []) path [idx])) 48 | (proto/render ast data) 49 | (sb/append sb))) 50 | (sb/new-string-builder)) 51 | (sb/to-string)))) 52 | 53 | (extend-type cljs.core/EmptyList 54 | proto/StandardSectionRenderer 55 | (render-section [l section data state] 56 | "") 57 | 58 | proto/InvertedSectionRenderer 59 | (render-inverted-section [l section data state] 60 | (proto/render-section-simply section data state))) 61 | 62 | (extend-type cljs.core/List 63 | proto/StandardSectionRenderer 64 | (render-section [l section data state] 65 | (render-section-seq l section data state)) 66 | 67 | proto/InvertedSectionRenderer 68 | (render-inverted-section [l section data state] 69 | (if (empty? l) 70 | (proto/render-section-simply section data state) 71 | ""))) 72 | 73 | (extend-type cljs.core/PersistentVector 74 | proto/StandardSectionRenderer 75 | (render-section [v section data state] 76 | (render-section-seq v section data state)) 77 | 78 | proto/InvertedSectionRenderer 79 | (render-inverted-section [v section data state] 80 | (if (empty? v) 81 | (proto/render-section-simply section data state) 82 | ""))) 83 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/ast.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.ast 2 | (:require [fast-zip.core :as zip] 3 | [clojure.string :as str] 4 | [org.panchromatic.mokuhan.util.stringbuilder :as sb])) 5 | 6 | (defprotocol ASTZipper 7 | (branch? [this]) 8 | (children [this]) 9 | (make-node [this children])) 10 | 11 | (extend-protocol ASTZipper 12 | #?(:clj Object :cljs default) 13 | (branch? [this] false) 14 | (children [this] nil) 15 | (make-node [this children] nil)) 16 | 17 | (defn to-visible [x] 18 | (assoc x :visible true)) 19 | 20 | (defn to-invisible [x] 21 | (assoc x :visible false)) 22 | 23 | (defn visible? [x] 24 | (:visible x true)) 25 | 26 | (defn- str-path [path] 27 | (str/join "." path)) 28 | 29 | (defn- wrap-delimiters [s {:keys [open close]}] 30 | (str open s close)) 31 | 32 | ;;; variable 33 | (defprotocol Variable) 34 | 35 | (defn variable? [x] 36 | (satisfies? Variable x)) 37 | 38 | (defrecord EscapedVariable [path delimiters] 39 | Variable 40 | 41 | Object 42 | (toString [_] 43 | (wrap-delimiters (str-path path) delimiters))) 44 | 45 | (defn new-escaped-variable [path delimiters] 46 | (EscapedVariable. (vec path) delimiters)) 47 | 48 | (defrecord UnescapedVariable [path delimiters] 49 | Variable 50 | 51 | Object 52 | (toString [_] 53 | (wrap-delimiters (str-path path) delimiters))) 54 | 55 | (defn new-unescaped-variable [path delimiters] 56 | (UnescapedVariable. (vec path) delimiters)) 57 | 58 | ;;; section 59 | (defn get-open-tag-delimiters [x] 60 | (:open-tag-delimiters x)) 61 | 62 | (defn set-close-tag-delimiters [x delimiters] 63 | (assoc x :close-tag-delimiters delimiters)) 64 | 65 | (defprotocol Section) 66 | 67 | (defn section? [x] 68 | (satisfies? Section x)) 69 | 70 | (defrecord StandardSection [path contents open-tag-delimiters close-tag-delimiters] 71 | Section 72 | 73 | ASTZipper 74 | (branch? [this] true) 75 | (children [this] contents) 76 | (make-node [this children] 77 | (StandardSection. path children open-tag-delimiters close-tag-delimiters)) 78 | 79 | Object 80 | (toString [_] 81 | (let [path (str-path path)] 82 | (-> (sb/new-string-builder) 83 | (sb/append (wrap-delimiters (str "#" path) open-tag-delimiters)) 84 | (as-> sb (reduce #(sb/append %1 (.toString %2)) sb contents)) 85 | (sb/append (wrap-delimiters (str "/" path) close-tag-delimiters)) 86 | (sb/to-string))))) 87 | 88 | (defn new-standard-section [path delimiters] 89 | (StandardSection. path () delimiters delimiters)) 90 | 91 | (defrecord InvertedSection [path contents open-tag-delimiters close-tag-delimiters] 92 | Section 93 | 94 | ASTZipper 95 | (branch? [this] true) 96 | (children [this] contents) 97 | (make-node [this children] 98 | (InvertedSection. path children open-tag-delimiters close-tag-delimiters)) 99 | 100 | Object 101 | (toString [_] 102 | (let [path (str-path path)] 103 | (-> (sb/new-string-builder) 104 | (sb/append (wrap-delimiters (str "#" path) open-tag-delimiters)) 105 | (as-> sb (reduce #(sb/append %1 (.toString %2)) sb contents)) 106 | (sb/append (wrap-delimiters (str "/" path) close-tag-delimiters)) 107 | (sb/to-string))))) 108 | 109 | (defn new-inverted-section [path delimiters] 110 | (InvertedSection. path () delimiters delimiters)) 111 | 112 | ;; other 113 | 114 | (defrecord BeginningOfLine [] ;marker 115 | Object 116 | (toString [_] "")) 117 | 118 | (def beginning-of-line 119 | (BeginningOfLine.)) 120 | 121 | (defn new-beginning-of-line [] 122 | beginning-of-line) 123 | 124 | (defn beginning-of-line? [x] 125 | (instance? BeginningOfLine x)) 126 | 127 | (defrecord Text [content] 128 | Object 129 | (toString [this] 130 | (if (visible? this) content ""))) 131 | 132 | (defn new-text [content] 133 | (Text. content)) 134 | 135 | (defrecord Whitespace [content] 136 | Object 137 | (toString [this] 138 | (if (visible? this) content ""))) 139 | 140 | (defn new-whitespace [content] 141 | (Whitespace. content)) 142 | 143 | (defn whitespace? [x] 144 | (instance? Whitespace x)) 145 | 146 | (defrecord Comment [content] 147 | Object 148 | (toString [this] "")) 149 | 150 | (defn new-comment [content] 151 | (Comment. content)) 152 | 153 | (defn comment? [x] 154 | (instance? Comment x)) 155 | 156 | (defrecord Mustache [contents] 157 | ASTZipper 158 | (branch? [this] true) 159 | (children [this] contents) 160 | (make-node [this children] 161 | (Mustache. children)) 162 | 163 | Object 164 | (toString [_] 165 | (sb/to-string 166 | (reduce #(sb/append %1 (.toString %2)) 167 | (sb/new-string-builder) 168 | contents)))) 169 | 170 | (defn new-mustache 171 | ([] 172 | (new-mustache ())) 173 | ([contents] 174 | (Mustache. contents))) 175 | 176 | (defn ast-zip 177 | ([] 178 | (ast-zip (new-mustache))) 179 | ([root] 180 | (zip/zipper branch? children make-node root))) 181 | -------------------------------------------------------------------------------- /test/org/panchromatic/mokuhan/renderer_test.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.renderer-test 2 | (:require [clojure.test :as t] 3 | [org.panchromatic.mokuhan.renderer :as sut] 4 | [org.panchromatic.mokuhan.ast :as ast])) 5 | 6 | (def ^:private delimiters 7 | {:open "{{" :close "}}"}) 8 | 9 | (t/deftest render-escaped-variable-test 10 | (t/testing "Single path" 11 | (let [v (ast/new-escaped-variable ["x"] delimiters)] 12 | (t/testing "String" 13 | (t/is (= "Hi" (sut/render v {:x "Hi"})))) 14 | 15 | (t/testing "Integer" 16 | (t/is (= "42" (sut/render v {:x 42})))) 17 | 18 | (t/testing "Boolean" 19 | (t/is (= "true" (sut/render v {:x true}))) 20 | (t/is (= "false" (sut/render v {:x false})))) 21 | 22 | (t/testing "HTML string" 23 | (t/is (= "&<>'"" (sut/render v {:x "&<>'\""})))) 24 | 25 | (t/testing "Map" 26 | (t/is (= "{:foo 1}" (sut/render v {:x {:foo 1}})))) 27 | 28 | (t/testing "Vector" 29 | (t/is (= "[1 2]" (sut/render v {:x [1 2]})))) 30 | 31 | (t/testing "Object" 32 | (t/is (= "object!" (sut/render v {:x (reify Object (toString [this] "object!"))})))) 33 | 34 | (t/testing "nil" 35 | (t/is (= "" (sut/render v {:x nil})))) 36 | 37 | (t/testing "missing" 38 | (t/is (= "" (sut/render v {})))))) 39 | 40 | (t/testing "Dotted path" 41 | (let [v (ast/new-escaped-variable ["x" "y"] delimiters)] 42 | (t/testing "String" 43 | (t/is (= "Hi" (sut/render v {:x {:y "Hi"}})))) 44 | 45 | (t/testing "Integer" 46 | (t/is (= "42" (sut/render v {:x {:y 42}})))) 47 | 48 | (t/testing "Boolean" 49 | (t/is (= "true" (sut/render v {:x {:y true}}))) 50 | (t/is (= "false" (sut/render v {:x {:y false}})))) 51 | 52 | (t/testing "HTML string" 53 | (t/is (= "&<>'"" (sut/render v {:x {:y "&<>'\""}})))) 54 | 55 | (t/testing "Map" 56 | (t/is (= "{:foo 1}" (sut/render v {:x {:y {:foo 1}}})))) 57 | 58 | (t/testing "Vector" 59 | (t/is (= "[1 2]" (sut/render v {:x {:y [1 2]}})))) 60 | 61 | (t/testing "nil" 62 | (t/is (= "" (sut/render v {:x {:y nil}})))) 63 | 64 | (t/testing "missing" 65 | (t/is (= "" (sut/render v {:x {}})))))) 66 | 67 | (t/testing "Include index of list" 68 | (let [v (ast/new-escaped-variable ["x" 1 "y"] delimiters)] 69 | (t/is (= "42" (sut/render v {:x [{:y 41} {:y 42}]}))) 70 | 71 | (t/is (= "" (sut/render v {:x [{:y 41}]}))))) 72 | 73 | (t/testing "Dot" 74 | (let [v (ast/new-escaped-variable ["."] delimiters)] 75 | (t/is (= "{:x 42}" (sut/render v {:x 42})))))) 76 | 77 | (t/deftest render-standard-section-test 78 | (t/testing "single path section" 79 | (let [v (-> (ast/new-standard-section ["x"] delimiters) 80 | (update :contents conj (ast/new-text "!!")))] 81 | (t/is (= "!!" 82 | (sut/render v {:x true}) 83 | (sut/render v {:x {}}) 84 | (sut/render v {:x 42}) 85 | (sut/render v {:x "Hello"}))) 86 | 87 | (t/is (= "" 88 | (sut/render v {:x false}) 89 | (sut/render v {:x []}) 90 | (sut/render v {:x nil}) 91 | (sut/render v {}) 92 | (sut/render v nil))) 93 | 94 | (t/is (= "!!!!" (sut/render v {:x [1 1]}))) 95 | 96 | (t/is (= "Hello!!" (sut/render v {:x #(str "Hello" %)}))))) 97 | 98 | (t/testing "dotted path section" 99 | (let [v (-> (ast/new-standard-section ["x" "y"] delimiters) 100 | (update :contents conj (ast/new-text "!!")))] 101 | (t/is (= "!!" 102 | (sut/render v {:x {:y true}}) 103 | (sut/render v {:x {:y {}}}) 104 | (sut/render v {:x {:y 42}}) 105 | (sut/render v {:x {:y "Hello"}}))) 106 | 107 | (t/is (= "" 108 | (sut/render v {:x {:y false}}) 109 | (sut/render v {:x {:y []}}) 110 | (sut/render v {:x {:y nil}}) 111 | (sut/render v {:x {}}) 112 | (sut/render v {:x nil}))) 113 | 114 | (t/is (= "!!!!" (sut/render v {:x {:y [1 1]}}))) 115 | 116 | (t/is (= "Hello!!" (sut/render v {:x {:y #(str "Hello" %)}}))))) 117 | 118 | (t/testing "nested section" 119 | (let [v (-> (ast/new-standard-section ["x"] delimiters) 120 | (update :contents conj (-> (ast/new-standard-section ["y"] delimiters) 121 | (update :contents conj (ast/new-text "!!")))))] 122 | (t/is (= "!!" (sut/render v {:x {:y true}}))) 123 | (t/is (= "!!!!" (sut/render v {:x {:y [1 1]}}))) 124 | (t/is (= "!!!!!!!!" (sut/render v {:x [{:y [1 1]} {:y [1 1]}]}))) 125 | (t/is (= "!!!!" 126 | (sut/render v {:x [{:y [1 1]} {:y []}]}) 127 | (sut/render v {:x [{:y true} {:y false} {:y true}]}))))) 128 | 129 | (t/testing "nested and don't use outer key" 130 | (let [v (-> [(-> (ast/new-standard-section ["x"] delimiters) 131 | (update :contents conj (-> (ast/new-standard-section ["y"] delimiters) 132 | (update :contents conj (ast/new-text "Hello")))))] 133 | ast/new-mustache)] 134 | (t/is (= "" (sut/render v {:x [{:y false}] 135 | :y true})))))) 136 | -------------------------------------------------------------------------------- /src/org/panchromatic/mokuhan/parser.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.parser 2 | (:require [clojure.string :as str] 3 | [fast-zip.core :as zip] 4 | [instaparse.core :as insta] 5 | [org.panchromatic.mokuhan.ast :as ast] 6 | [org.panchromatic.mokuhan.util.misc :as misc] 7 | [org.panchromatic.mokuhan.util.regex :as regex] 8 | [org.panchromatic.mokuhan.walker :as walker])) 9 | 10 | ;;; {{name}} -> variable 11 | ;;; {{{name}}} -> unescaped variable 12 | ;;; {{&name}} -> unescaped variable 13 | ;;; {{#persone}} <-> {{/person}} -> section 14 | ;;; false or empty list -> delete 15 | ;;; non empty list -> repeat 16 | ;;; lambda -> call function 17 | ;;; non-false -> context 18 | ;;; {{^name}} <-> {{/name}} -> inverted variable 19 | ;;; {{! blah }} -> comment 20 | ;;; {{> box}} -> partial 21 | ;;; {{=<% %>=}} -> set delimiter 22 | 23 | (def default-delimiters 24 | {:open "{{" :close "}}"}) 25 | 26 | (def ^:private sigils ["\\&" "\\#" "\\/" "\\^" "\\>"]) 27 | 28 | (defn generate-mustache-spec [{:keys [open close] :as delimiters}] 29 | (str " 30 | = *(beginning-of-line *(text / whitespace / tag) end-of-line) 31 | beginning-of-line = <#'(?:^" #?(:clj "|\\A") ")'> 32 | end-of-line = #'(?:\\r?\\n|" #?(:clj "\\z" :cljs "$") ")' 33 | text = !tag #'[^\\r\\n\\s]+?(?=(?:" (regex/source (regex/re-quote open)) "|\\r?\\n|\\s|" #?(:clj "\\z" :cljs "$") "))' 34 | whitespace = #'[^\\S\\r\\n]+' 35 | 36 | = #'(?!(?:\\!|\\=))[^\\s\\.]+?(?=\\s|\\.|" (regex/source (regex/re-quote close)) ")' 37 | path = (ident *(<'.'> ident) / #'\\.') 38 | 39 | = ( comment-tag / set-delimiter-tag / standard-tag / alt-unescaped-tag ) 40 | tag-open = #'" (regex/source (regex/re-quote open)) "' 41 | tag-close = #'" (regex/source (regex/re-quote close)) "' 42 | standard-tag = tag-open sigil <*1 #'\\s+'> path <*1 #'\\s+'> tag-close 43 | sigil = #'(?:" (str/join "|" sigils) ")?' 44 | alt-unescaped-tag = tag-open #'\\{' <*1 #'\\s+'> path <*1 #'\\s+'> #'\\}' tag-close 45 | 46 | comment-tag = tag-open <#'!'> comment-content tag-close 47 | comment-content = #'(?:.|\\r?\\n)*?(?=" (regex/source (regex/re-quote close)) ")' 48 | 49 | set-delimiter-tag = tag-open <#'='> <*1 #'\\s+'> new-open-delimiter <*1 #'\\s+'> new-close-delimiter <*1 #'\\s+'> <#'='> tag-close rest 50 | new-open-delimiter = #'[^\\s]+' 51 | new-close-delimiter = #'[^\\s]+?(?=\\s*" (regex/source (regex/re-quote (str "=" close))) ")' 52 | rest = #'(.|\\r?\\n)*$'")) 53 | 54 | (defn gen-parser [delimiters] 55 | (insta/parser (generate-mustache-spec delimiters) 56 | :input-format :abnf)) 57 | 58 | (def default-parser 59 | (gen-parser default-delimiters)) 60 | 61 | (defn parse* 62 | ([mustache] 63 | (parse* mustache {})) 64 | ([mustache opts] 65 | (let [delimiters (:delimiters opts default-delimiters) 66 | parser (if (= default-delimiters delimiters) 67 | default-parser 68 | (gen-parser delimiters))] 69 | (->> (dissoc opts :parser) 70 | (reduce-kv #(conj %1 %2 %3) []) 71 | (apply insta/parse parser mustache))))) 72 | 73 | (defn- invisible-rightside-children-whitespaces [loc] 74 | (if (zip/down loc) 75 | (loop [loc (some-> loc zip/down zip/rightmost)] 76 | (if (and loc (ast/whitespace? (zip/node loc))) 77 | (-> (zip/edit loc ast/to-invisible) 78 | zip/left 79 | recur) 80 | (zip/up loc))) 81 | loc)) 82 | 83 | (defn- copy-left-whitespaces [loc] 84 | (loop [loc (zip/left loc) 85 | whitespaces []] 86 | (if (ast/whitespace? (zip/node loc)) 87 | (recur (zip/left loc) (conj whitespaces (zip/node loc))) 88 | whitespaces))) 89 | 90 | (defn parse 91 | ([mustache] 92 | (parse mustache {})) 93 | ([mustache opts] 94 | (loop [loc (ast/ast-zip) 95 | [elm & parsed] (parse* mustache opts) 96 | state {:stack [] ;; for section balance 97 | :standalone? true ;; for standalone tag 98 | }] 99 | (if (nil? elm) 100 | (if (zip/up loc) 101 | (throw (ex-info "Unclosed section" 102 | {:type ::unclosed-section 103 | :tag (peek (:stack state)) 104 | :meta (misc/meta-without-qualifiers elm)})) 105 | (zip/root loc)) 106 | 107 | (case (first elm) 108 | :standard-tag 109 | (let [[_ [_ open] [_ sigil] [_ & path] [_ close]] elm 110 | delimiters {:open open :close close}] 111 | (case sigil 112 | ("#" "^") ;; open section 113 | (let [standalone? (and (:standalone? state) (= :end-of-line (ffirst parsed)))] 114 | (recur (-> (cond-> loc standalone? invisible-rightside-children-whitespaces) 115 | (zip/append-child (if (= "#" sigil) 116 | (ast/new-standard-section path delimiters) 117 | (ast/new-inverted-section path delimiters))) 118 | zip/down 119 | zip/rightmost) 120 | (cond->> parsed standalone? (drop 2)) ;; `drop 2` means remove EOL&BOL 121 | (-> state 122 | (update :stack conj path) 123 | (assoc :standalone? standalone?)))) 124 | 125 | "/" ;; close secion 126 | (if (= (peek (:stack state)) path) 127 | (let [standalone? (and (:standalone? state) (= :end-of-line (ffirst parsed)))] 128 | (recur (-> (cond-> loc standalone? invisible-rightside-children-whitespaces) 129 | (zip/edit ast/set-close-tag-delimiters delimiters) 130 | zip/up) 131 | (cond->> parsed standalone? (drop 2)) 132 | (-> state 133 | (update :stack pop) 134 | (assoc :standalone? standalone?)))) 135 | (throw (ex-info "Unopened section" 136 | {:type ::unopend-section 137 | :tag path 138 | :meta (misc/meta-without-qualifiers elm)}))) 139 | 140 | ">" ;; partial 141 | (let [standalone? (and (:standalone? state) (= :end-of-line (ffirst parsed))) 142 | whitespaces (when standalone? 143 | (-> loc 144 | (zip/append-child nil) 145 | zip/down 146 | zip/rightmost 147 | copy-left-whitespaces)) 148 | children (-> (:partials opts) 149 | (walker/traverse path []) 150 | (parse opts) 151 | (ast/children) 152 | (->> (drop 1)))] 153 | (recur (reduce #(-> %1 154 | (cond-> (ast/beginning-of-line? %2) 155 | (as-> loc' (reduce (fn [l ws] (zip/append-child l ws)) loc' whitespaces))) 156 | (zip/append-child %2)) 157 | loc 158 | children) 159 | (cond->> parsed standalone? (drop 2)) 160 | state)) 161 | 162 | (recur (-> loc (zip/append-child (if (= "" sigil) 163 | (ast/new-escaped-variable path delimiters) 164 | (ast/new-unescaped-variable path delimiters)))) 165 | parsed 166 | (assoc state :standalone? false)))) 167 | 168 | :alt-unescaped-tag 169 | (let [[_ [_ open] _ [_ & path] _ [_ close]] elm 170 | delimiters {:open open :close close}] 171 | (recur (-> loc (zip/append-child (ast/new-unescaped-variable path delimiters))) 172 | parsed 173 | (assoc state :standalone? false))) 174 | 175 | :set-delimiter-tag 176 | (let [[_ _ [_ open] [_ close] _ [_ rest-of-mustache]] elm 177 | delimiters {:open open :close close} 178 | parsed (->> (parse* rest-of-mustache {:delimiters delimiters}) 179 | (drop 1) ;; don't need BOL 180 | ) 181 | standalone? (and (:standalone? state) 182 | (or (= :end-of-line (ffirst parsed)) (empty? parsed)))] 183 | (recur (cond-> loc standalone? invisible-rightside-children-whitespaces) 184 | (cond->> parsed standalone? (drop 2)) 185 | (assoc state :standalone? standalone?))) 186 | 187 | :comment-tag 188 | (let [standalone? (and (:standalone? state) (= :end-of-line (ffirst parsed))) 189 | [_ _ comment-content _] elm] 190 | (recur (-> (cond-> loc standalone? invisible-rightside-children-whitespaces) 191 | (zip/append-child (ast/new-comment comment-content))) 192 | (cond->> parsed standalone? (drop 2)) 193 | (assoc state :standalone? standalone?))) 194 | 195 | :whitespace 196 | (recur (-> loc (zip/append-child (ast/new-whitespace (second elm)))) 197 | parsed 198 | ;; keep current state 199 | state) 200 | 201 | :beginning-of-line 202 | (recur (-> loc (zip/append-child (ast/new-beginning-of-line))) 203 | parsed 204 | (assoc state :standalone? true)) 205 | 206 | (recur (-> loc (zip/append-child (ast/new-text (second elm)))) 207 | parsed 208 | (assoc state :standalone? false))))))) 209 | -------------------------------------------------------------------------------- /test/org/panchromatic/mokuhan/parser_test.clj: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan.parser-test 2 | (:require [clojure.test :as t] 3 | [fast-zip.core :as zip] 4 | [org.panchromatic.mokuhan.ast :as ast] 5 | [org.panchromatic.mokuhan.parser :as sut])) 6 | 7 | (comment 8 | (t/deftest parse-variables-test 9 | (t/testing "escaped variables" 10 | (t/is 11 | (= #org.panchromatic.mokuhan.ast.Mustache 12 | {:contents 13 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 14 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["x"]} 15 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 16 | (sut/parse "{{x}}") 17 | (sut/parse "{{ x }}") 18 | (sut/parse "{{\tx\t}}") 19 | (sut/parse "{{\nx\n}}"))) 20 | 21 | (t/is 22 | (= #org.panchromatic.mokuhan.ast.Mustache 23 | {:contents 24 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 25 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["x" "y"]} 26 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 27 | (sut/parse "{{x.y}}") 28 | (sut/parse "{{ x.y }}") 29 | (sut/parse "{{\tx.y\t}}") 30 | (sut/parse "{{\nx.y\n}}"))) 31 | 32 | (t/is 33 | (= #org.panchromatic.mokuhan.ast.Mustache 34 | {:contents 35 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 36 | #org.panchromatic.mokuhan.ast.Whitespace{:content " "} 37 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["x"]} 38 | #org.panchromatic.mokuhan.ast.Whitespace{:content " "} 39 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 40 | (sut/parse " {{ x }} "))) 41 | 42 | (t/is 43 | (= #org.panchromatic.mokuhan.ast.Mustache 44 | {:contents 45 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 46 | #org.panchromatic.mokuhan.ast.Text{:content "--"} 47 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["x"]} 48 | #org.panchromatic.mokuhan.ast.Text{:content "--"} 49 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 50 | (sut/parse "--{{x}}--"))) 51 | 52 | (t/is 53 | (= #org.panchromatic.mokuhan.ast.Mustache 54 | {:contents 55 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 56 | #org.panchromatic.mokuhan.ast.Text{:content "}}"} 57 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["x"]} 58 | #org.panchromatic.mokuhan.ast.Text{:content "--"} 59 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 60 | (sut/parse "}}{{x}}--"))) 61 | 62 | (t/is 63 | (= #org.panchromatic.mokuhan.ast.Mustache 64 | {:contents 65 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 66 | #org.panchromatic.mokuhan.ast.Text{:content "--"} 67 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["x"]} 68 | #org.panchromatic.mokuhan.ast.Text{:content "{{"} 69 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 70 | (sut/parse "--{{x}}{{"))) 71 | 72 | (t/is 73 | (= #org.panchromatic.mokuhan.ast.Mustache 74 | {:contents 75 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 76 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["x"]} 77 | #org.panchromatic.mokuhan.ast.Text{:content "}"} 78 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 79 | (sut/parse "{{x}}}"))) 80 | 81 | (t/is 82 | (= #org.panchromatic.mokuhan.ast.Mustache 83 | {:contents 84 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 85 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["{x"]} 86 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 87 | (sut/parse "{{{x}}")))) 88 | 89 | 90 | (t/testing "unescaped variables" 91 | (t/is 92 | (= #org.panchromatic.mokuhan.ast.Mustache 93 | {:contents 94 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 95 | #org.panchromatic.mokuhan.ast.UnescapedVariable{:path ["x"]} 96 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 97 | (sut/parse "{{{x}}}") 98 | (sut/parse "{{{ x }}}") 99 | (sut/parse "{{{\tx\t}}}") 100 | (sut/parse "{{{\nx\n}}}"))) 101 | 102 | (t/is 103 | (= #org.panchromatic.mokuhan.ast.Mustache 104 | {:contents 105 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 106 | #org.panchromatic.mokuhan.ast.UnescapedVariable{:path ["x" "y"]} 107 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 108 | (sut/parse "{{{x.y}}}") 109 | (sut/parse "{{{ x.y }}}") 110 | (sut/parse "{{{\tx.y\t}}}") 111 | (sut/parse "{{{\nx.y\n}}}"))) 112 | 113 | (t/is 114 | (= #org.panchromatic.mokuhan.ast.Mustache 115 | {:contents 116 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 117 | #org.panchromatic.mokuhan.ast.UnescapedVariable{:path ["x"]} 118 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 119 | (sut/parse "{{&x}}") 120 | (sut/parse "{{& x }}") 121 | (sut/parse "{{&\tx\t}}") 122 | (sut/parse "{{&\nx\n}}"))) 123 | 124 | (t/is 125 | (= #org.panchromatic.mokuhan.ast.Mustache 126 | {:contents 127 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 128 | #org.panchromatic.mokuhan.ast.UnescapedVariable{:path ["x" "y"]} 129 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 130 | (sut/parse "{{&x.y}}") 131 | (sut/parse "{{& x.y }}") 132 | (sut/parse "{{&\tx.y\t}}") 133 | (sut/parse "{{&\nx.y\n}}"))) 134 | 135 | (t/is 136 | (= #org.panchromatic.mokuhan.ast.Mustache 137 | {:contents 138 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 139 | #org.panchromatic.mokuhan.ast.Whitespace{:content " "} 140 | #org.panchromatic.mokuhan.ast.UnescapedVariable{:path ["x"]} 141 | #org.panchromatic.mokuhan.ast.Whitespace{:content " "} 142 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 143 | (sut/parse " {{{x}}} ") 144 | (sut/parse " {{&x}} "))) 145 | 146 | (t/is 147 | (= #org.panchromatic.mokuhan.ast.Mustache 148 | {:contents 149 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 150 | #org.panchromatic.mokuhan.ast.Text{:content "--"} 151 | #org.panchromatic.mokuhan.ast.UnescapedVariable{:path ["x"]} 152 | #org.panchromatic.mokuhan.ast.Text{:content "--"} 153 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 154 | (sut/parse "--{{{x}}}--") 155 | (sut/parse "--{{&x}}--"))) 156 | 157 | (t/is 158 | (= #org.panchromatic.mokuhan.ast.Mustache 159 | {:contents 160 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 161 | #org.panchromatic.mokuhan.ast.UnescapedVariable{:path ["{x"]} 162 | #org.panchromatic.mokuhan.ast.Text{:content "}"} 163 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 164 | (sut/parse "{{{{x}}}}"))) 165 | 166 | (t/is 167 | (= #org.panchromatic.mokuhan.ast.Mustache 168 | {:contents 169 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 170 | #org.panchromatic.mokuhan.ast.UnescapedVariable{:path ["&x"]} 171 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 172 | (sut/parse "{{{ &x }}}"))) 173 | 174 | (t/is 175 | (= #org.panchromatic.mokuhan.ast.Mustache 176 | {:contents 177 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 178 | #org.panchromatic.mokuhan.ast.UnescapedVariable{:path ["&x"]} 179 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 180 | (sut/parse "{{{ &x }}}"))) 181 | 182 | (t/is 183 | (= #org.panchromatic.mokuhan.ast.Mustache 184 | {:contents 185 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 186 | #org.panchromatic.mokuhan.ast.Text{:content "{"} 187 | #org.panchromatic.mokuhan.ast.UnescapedVariable{:path ["x"]} 188 | #org.panchromatic.mokuhan.ast.Text{:content "}"} 189 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 190 | (sut/parse "{{{& x }}}")))))) 191 | 192 | (defn- find-nth-tag [ast n] 193 | (loop [loc (ast/ast-zip ast), n n] 194 | (when-not (zip/end? loc) 195 | (let [node (zip/node loc)] 196 | (if ((some-fn ast/variable? ast/section?) node) 197 | (if (zero? n) 198 | node 199 | (recur (zip/next loc) (dec n))) 200 | (recur (zip/next loc) n)))))) 201 | 202 | (defn- find-first-tag [ast] 203 | (find-nth-tag ast 0)) 204 | 205 | (comment 206 | (t/deftest parse-name-test 207 | (t/testing "single name" 208 | (t/are [src expected] (= expected (.path (find-first-tag (sut/parse src)))) 209 | "{{x}}" ["x"] 210 | " {{x}} " ["x"] 211 | "{{ x }}" ["x"] 212 | "{{\n\nx\n\n}}" ["x"])) 213 | 214 | (t/testing "dotted name" 215 | (t/are [src expected] (= expected (.path (find-first-tag (sut/parse src)))) 216 | "{{x.y}}" ["x" "y"] 217 | " {{x.y}} " ["x" "y"] 218 | "{{ x.y }}" ["x" "y"] 219 | "{{x.y.z}}" ["x" "y" "z"] 220 | "{{\n\nx.y.z\n\n}}" ["x" "y" "z"])) 221 | 222 | (t/testing "current context" 223 | (t/are [src expected] (= expected (.path (find-first-tag (sut/parse src)))) 224 | "{{.}}" ["."] 225 | " {{.}} " ["."] 226 | "{{ . }}" ["."] 227 | "{{\n.\n}}" ["."])) 228 | 229 | (t/testing "illegal names" 230 | (t/are [src] (nil? (find-first-tag (sut/parse* src))) 231 | "{{.x}}" 232 | "{{x.}}" 233 | "{{.x.}}" 234 | "{{x . y}}" 235 | "{{x. y}}" 236 | "{{x .y}}") 237 | 238 | (t/is 239 | (nil? (find-nth-tag (sut/parse "{{x}} {{.y}}") 1))) 240 | 241 | (t/is 242 | (not= ["x"] (.path (find-first-tag (sut/parse "{{.x}} {{y}}"))))) 243 | 244 | (t/is (= [["x"] ["z"]] 245 | (as-> (sut/parse "{{x}} {{.y}} {{z}}") ast 246 | [(.path (find-nth-tag ast 0)) 247 | (.path (find-nth-tag ast 1))]))) 248 | 249 | (t/is (= [["y"] nil] 250 | (as-> (sut/parse "{{.x}} {{y}} {{.z}}") ast 251 | [(.path (find-nth-tag ast 0)) 252 | (some-> (find-nth-tag ast 1) .path)])))))) 253 | 254 | (comment 255 | (t/deftest parse-section-test 256 | (t/testing "standard section" 257 | (t/is 258 | (= #org.panchromatic.mokuhan.ast.Mustache 259 | {:contents 260 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 261 | #org.panchromatic.mokuhan.ast.StandardSection{:path ["x"], :contents nil} 262 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 263 | (sut/parse "{{#x}}{{/x}}"))) 264 | 265 | (t/is 266 | (= #org.panchromatic.mokuhan.ast.Mustache 267 | {:contents 268 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 269 | #org.panchromatic.mokuhan.ast.StandardSection{:path ["x" "y"], :contents nil} 270 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 271 | (sut/parse "{{#x.y}}{{/x.y}}"))) 272 | 273 | (t/is 274 | (= #org.panchromatic.mokuhan.ast.Mustache 275 | {:contents 276 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 277 | #org.panchromatic.mokuhan.ast.StandardSection 278 | {:path ["x"], 279 | :contents 280 | (#org.panchromatic.mokuhan.ast.StandardSection{:path ["y"], :contents nil})} 281 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 282 | (sut/parse "{{#x}}{{#y}}{{/y}}{{/x}}"))) 283 | 284 | (t/is 285 | (= #org.panchromatic.mokuhan.ast.Mustache 286 | {:contents 287 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 288 | #org.panchromatic.mokuhan.ast.StandardSection 289 | {:path ["x"], 290 | :contents (#org.panchromatic.mokuhan.ast.Text{:content "}}"})} 291 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 292 | (sut/parse "{{#x}}}}{{/x}}")))) 293 | 294 | (t/testing "inverted section" 295 | (t/is 296 | (= #org.panchromatic.mokuhan.ast.Mustache 297 | {:contents 298 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 299 | #org.panchromatic.mokuhan.ast.InvertedSection{:path ["x"], :contents nil} 300 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 301 | (sut/parse "{{^x}}{{/x}}"))) 302 | 303 | (t/is 304 | (= #org.panchromatic.mokuhan.ast.Mustache 305 | {:contents 306 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 307 | #org.panchromatic.mokuhan.ast.InvertedSection{:path ["x" "y"], :contents nil} 308 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 309 | (sut/parse "{{^x.y}}{{/x.y}}")))) 310 | 311 | (t/testing "unopened section" 312 | (t/is 313 | (thrown-with-msg? 314 | clojure.lang.ExceptionInfo #"Unopened section" 315 | (sut/parse "{{/x}}"))) 316 | 317 | (t/is 318 | (thrown-with-msg? 319 | clojure.lang.ExceptionInfo #"Unopened section" 320 | (sut/parse "{{x}}{{/x}}")))) 321 | 322 | (t/testing "unclosed section" 323 | (t/is 324 | (thrown-with-msg? 325 | clojure.lang.ExceptionInfo #"Unclosed section" 326 | (sut/parse "{{#x}}"))) 327 | 328 | (t/is 329 | (thrown-with-msg? 330 | clojure.lang.ExceptionInfo #"Unclosed section" 331 | (sut/parse "{{#x}}{{x}}"))) 332 | 333 | (t/is 334 | (thrown-with-msg? 335 | clojure.lang.ExceptionInfo #"Unclosed section" 336 | (sut/parse "{{^x}}"))) 337 | 338 | (t/is 339 | (thrown-with-msg? 340 | clojure.lang.ExceptionInfo #"Unclosed section" 341 | (sut/parse "{{^x}}{{x}}")))))) 342 | 343 | (comment 344 | (t/deftest parse-comment-test 345 | (t/is 346 | (= #org.panchromatic.mokuhan.ast.Mustache 347 | {:contents 348 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 349 | #org.panchromatic.mokuhan.ast.Comment{:content "x"})} 350 | (sut/parse "{{!x}}"))) 351 | 352 | (t/is 353 | (= #org.panchromatic.mokuhan.ast.Mustache 354 | {:contents 355 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 356 | #org.panchromatic.mokuhan.ast.Comment{:content " x "})} 357 | (sut/parse "{{! x }}"))) 358 | 359 | (t/is 360 | (= #org.panchromatic.mokuhan.ast.Mustache 361 | {:contents 362 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 363 | #org.panchromatic.mokuhan.ast.Comment{:content "\n\nx\n\n"})} 364 | (sut/parse "{{!\n\nx\n\n}}"))) 365 | 366 | (t/is 367 | (= #org.panchromatic.mokuhan.ast.Mustache 368 | {:contents 369 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 370 | #org.panchromatic.mokuhan.ast.Comment{:content " x y z " })} 371 | (sut/parse "{{! x y z }}"))) 372 | 373 | (t/is 374 | (= #org.panchromatic.mokuhan.ast.Mustache 375 | {:contents 376 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 377 | #org.panchromatic.mokuhan.ast.Comment{:content " x {{x"} 378 | #org.panchromatic.mokuhan.ast.Text{:content "}}"} 379 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 380 | (sut/parse "{{! x {{x}}}}"))) 381 | 382 | (t/is 383 | (= #org.panchromatic.mokuhan.ast.Mustache 384 | {:contents 385 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 386 | #org.panchromatic.mokuhan.ast.Comment{:content "{{x"} 387 | #org.panchromatic.mokuhan.ast.Whitespace{:content " "} 388 | #org.panchromatic.mokuhan.ast.Text{:content "x}}"} 389 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 390 | (sut/parse "{{!{{x}} x}}"))))) 391 | 392 | (comment 393 | (t/deftest parse-set-delimiter-test 394 | (t/is (= #org.panchromatic.mokuhan.ast.Mustache 395 | {:contents 396 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{})} 397 | (sut/parse "{{=<< >>=}}"))) 398 | 399 | (t/is (= #org.panchromatic.mokuhan.ast.Mustache 400 | {:contents 401 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{})} 402 | (sut/parse "{{={% %}=}}"))) 403 | 404 | (t/is (= #org.panchromatic.mokuhan.ast.Mustache 405 | {:contents 406 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{})} 407 | (sut/parse "{{= % % =}}"))) 408 | 409 | (t/is (= #org.panchromatic.mokuhan.ast.Mustache 410 | {:contents 411 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 412 | #org.panchromatic.mokuhan.ast.Text{:content "{{x}}"} 413 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 414 | (sut/parse "{{=% %=}}{{x}}"))) 415 | 416 | (t/is (= #org.panchromatic.mokuhan.ast.Mustache 417 | {:contents 418 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 419 | #org.panchromatic.mokuhan.ast.Text{:content "=}}"} 420 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 421 | (sut/parse "{{=% %=}}=}}"))) 422 | 423 | (t/is (= #org.panchromatic.mokuhan.ast.Mustache 424 | {:contents 425 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 426 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["x"]} 427 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 428 | (sut/parse "{{=% %=}}\n%x%") 429 | #org.panchromatic.mokuhan.ast.Mustache 430 | {:contents 431 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 432 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["x"]} 433 | #org.panchromatic.mokuhan.ast.Text{:content ""})})))) 434 | 435 | (comment 436 | (t/deftest parse-newline-test 437 | (t/is (= #org.panchromatic.mokuhan.ast.Mustache 438 | {:contents 439 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 440 | #org.panchromatic.mokuhan.ast.Text{:content "\n"})} 441 | (sut/parse "\n"))) 442 | 443 | (t/is (= #org.panchromatic.mokuhan.ast.Mustache 444 | {:contents 445 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 446 | #org.panchromatic.mokuhan.ast.Text{:content "\r\n"})} 447 | (sut/parse "\r\n"))))) 448 | 449 | (comment 450 | (t/deftest parser-test 451 | (t/is 452 | (= #org.panchromatic.mokuhan.ast.Mustache 453 | {:contents 454 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 455 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["x"]} 456 | #org.panchromatic.mokuhan.ast.Whitespace{:content " "} 457 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["y"]} 458 | #org.panchromatic.mokuhan.ast.Whitespace{:content " "} 459 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["z"]} 460 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 461 | (sut/parse "{{x}} {{y}} {{z}}"))) 462 | 463 | (t/is 464 | (= #org.panchromatic.mokuhan.ast.Mustache 465 | {:contents 466 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 467 | #org.panchromatic.mokuhan.ast.StandardSection 468 | {:path ["person"], 469 | :contents 470 | (#org.panchromatic.mokuhan.ast.Whitespace{:content " "} 471 | #org.panchromatic.mokuhan.ast.EscapedVariable{:path ["name"]} 472 | #org.panchromatic.mokuhan.ast.Whitespace{:content " "})} 473 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 474 | (sut/parse "{{#person}} {{name}} {{/person}}"))) 475 | 476 | (mapcat #(map inc %) [(range 10)(range 10)]) 477 | 478 | (t/is 479 | (= #org.panchromatic.mokuhan.ast.Mustache 480 | {:contents 481 | (#org.panchromatic.mokuhan.ast.BeginningOfLine{} 482 | #org.panchromatic.mokuhan.ast.InvertedSection 483 | {:path ["person"], 484 | :contents 485 | (#org.panchromatic.mokuhan.ast.Whitespace{:content " "} 486 | #org.panchromatic.mokuhan.ast.Text{:content "Nothing"} 487 | #org.panchromatic.mokuhan.ast.Whitespace{:content " "})} 488 | #org.panchromatic.mokuhan.ast.Text{:content ""})} 489 | (sut/parse "{{^person}} Nothing {{/person}}"))) 490 | 491 | (t/is (thrown-with-msg? 492 | clojure.lang.ExceptionInfo #"Unopened section" 493 | (sut/parse "{{name}} {{/person}}"))) 494 | 495 | (t/is (thrown-with-msg? 496 | clojure.lang.ExceptionInfo #"Unopened section" 497 | (sut/parse "{{#x}}{{/y}}"))) 498 | 499 | (t/is (thrown-with-msg? 500 | clojure.lang.ExceptionInfo #"Unclosed section" 501 | (sut/parse "{{#person}} {{name}}"))) 502 | 503 | (t/is (thrown-with-msg? 504 | clojure.lang.ExceptionInfo #"Unclosed section" 505 | (sut/parse "{{^person}} Nothing "))) 506 | 507 | (t/is (thrown-with-msg? 508 | clojure.lang.ExceptionInfo #"Unclosed section" 509 | (sut/parse "{{#x}}{{{{/x}}"))))) 510 | -------------------------------------------------------------------------------- /test/org/panchromatic/mokuhan_test.cljc: -------------------------------------------------------------------------------- 1 | (ns org.panchromatic.mokuhan-test 2 | (:require [clojure.test :as t] 3 | [clojure.walk :as walk] 4 | [org.panchromatic.mokuhan :as sut] 5 | [org.panchromatic.mokuhan.test-helper :as h :include-macros true])) 6 | 7 | ;; (h/generate-test-cases-from-spec comments) 8 | 9 | (t/deftest comments-test 10 | (t/testing 11 | "Inline / Comment blocks should be removed from the template." 12 | (t/is 13 | (= 14 | "1234567890" 15 | (sut/render "12345{{! Comment Block! }}67890" {} {})))) 16 | (t/testing 17 | "Multiline / Multiline comments should be permitted." 18 | (t/is 19 | (= 20 | "1234567890\n" 21 | (sut/render 22 | "12345{{!\n This is a\n multi-line comment...\n}}67890\n" 23 | {} 24 | {})))) 25 | (t/testing 26 | "Standalone / All standalone comment lines should be removed." 27 | (t/is 28 | (= 29 | "Begin.\nEnd.\n" 30 | (sut/render "Begin.\n{{! Comment Block! }}\nEnd.\n" {} {})))) 31 | (t/testing 32 | "Indented Standalone / All standalone comment lines should be removed." 33 | (t/is 34 | (= 35 | "Begin.\nEnd.\n" 36 | (sut/render 37 | "Begin.\n {{! Indented Comment Block! }}\nEnd.\n" 38 | {} 39 | {})))) 40 | (t/testing 41 | "Standalone Line Endings / \"\\r\\n\" should be considered a newline for standalone tags." 42 | (t/is 43 | (= 44 | "|\r\n|" 45 | (sut/render "|\r\n{{! Standalone Comment }}\r\n|" {} {})))) 46 | (t/testing 47 | "Standalone Without Previous Line / Standalone tags should not require a newline to precede them." 48 | (t/is 49 | (= "!" (sut/render " {{! I'm Still Standalone }}\n!" {} {})))) 50 | (t/testing 51 | "Standalone Without Newline / Standalone tags should not require a newline to follow them." 52 | (t/is 53 | (= "!\n" (sut/render "!\n {{! I'm Still Standalone }}" {} {})))) 54 | (t/testing 55 | "Multiline Standalone / All standalone comment lines should be removed." 56 | (t/is 57 | (= 58 | "Begin.\nEnd.\n" 59 | (sut/render 60 | "Begin.\n{{!\nSomething's going on here...\n}}\nEnd.\n" 61 | {} 62 | {})))) 63 | (t/testing 64 | "Indented Multiline Standalone / All standalone comment lines should be removed." 65 | (t/is 66 | (= 67 | "Begin.\nEnd.\n" 68 | (sut/render 69 | "Begin.\n {{!\n Something's going on here...\n }}\nEnd.\n" 70 | {} 71 | {})))) 72 | (t/testing 73 | "Indented Inline / Inline comments should not strip whitespace" 74 | (t/is (= " 12 \n" (sut/render " 12 {{! 34 }}\n" {} {})))) 75 | (t/testing 76 | "Surrounding Whitespace / Comment removal should preserve surrounding whitespace." 77 | (t/is 78 | (= 79 | "12345 67890" 80 | (sut/render "12345 {{! Comment Block! }} 67890" {} {}))))) 81 | 82 | ;; (h/generate-test-cases-from-spec delimiters) 83 | 84 | (t/deftest delimiters-test 85 | (t/testing 86 | "Pair Behavior / The equals sign (used on both sides) should permit delimiter changes." 87 | (t/is 88 | (= 89 | "(Hey!)" 90 | (sut/render "{{=<% %>=}}(<%text%>)" {:text "Hey!"} {})))) 91 | (t/testing 92 | "Special Characters / Characters with special meaning regexen should be valid delimiters." 93 | (t/is 94 | (= 95 | "(It worked!)" 96 | (sut/render "({{=[ ]=}}[text])" {:text "It worked!"} {})))) 97 | (t/testing 98 | "Sections / Delimiters set outside sections should persist." 99 | (t/is 100 | (= 101 | "[\n I got interpolated.\n |data|\n\n {{data}}\n I got interpolated.\n]\n" 102 | (sut/render 103 | "[\n{{#section}}\n {{data}}\n |data|\n{{/section}}\n\n{{= | | =}}\n|#section|\n {{data}}\n |data|\n|/section|\n]\n" 104 | {:section true, :data "I got interpolated."} 105 | {})))) 106 | (t/testing 107 | "Inverted Sections / Delimiters set outside inverted sections should persist." 108 | (t/is 109 | (= 110 | "[\n I got interpolated.\n |data|\n\n {{data}}\n I got interpolated.\n]\n" 111 | (sut/render 112 | "[\n{{^section}}\n {{data}}\n |data|\n{{/section}}\n\n{{= | | =}}\n|^section|\n {{data}}\n |data|\n|/section|\n]\n" 113 | {:section false, :data "I got interpolated."} 114 | {})))) 115 | (t/testing 116 | "Partial Inheritence / Delimiters set in a parent template should not affect a partial." 117 | (t/is 118 | (= 119 | "[ .yes. ]\n[ .yes. ]\n" 120 | (sut/render 121 | "[ {{>include}} ]\n{{= | | =}}\n[ |>include| ]\n" 122 | {:value "yes"} 123 | {:partials {:include ".{{value}}."}})))) 124 | (t/testing 125 | "Post-Partial Behavior / Delimiters set in a partial should not affect the parent template." 126 | (t/is 127 | (= 128 | "[ .yes. .yes. ]\n[ .yes. .|value|. ]\n" 129 | (sut/render 130 | "[ {{>include}} ]\n[ .{{value}}. .|value|. ]\n" 131 | {:value "yes"} 132 | {:partials 133 | {:include ".{{value}}. {{= | | =}} .|value|."}})))) 134 | (t/testing 135 | "Surrounding Whitespace / Surrounding whitespace should be left untouched." 136 | (t/is (= "| |" (sut/render "| {{=@ @=}} |" {} {})))) 137 | (t/testing 138 | "Outlying Whitespace (Inline) / Whitespace should be left untouched." 139 | (t/is (= " | \n" (sut/render " | {{=@ @=}}\n" {} {})))) 140 | (t/testing 141 | "Standalone Tag / Standalone lines should be removed from the template." 142 | (t/is 143 | (= 144 | "Begin.\nEnd.\n" 145 | (sut/render "Begin.\n{{=@ @=}}\nEnd.\n" {} {})))) 146 | (t/testing 147 | "Indented Standalone Tag / Indented standalone lines should be removed from the template." 148 | (t/is 149 | (= 150 | "Begin.\nEnd.\n" 151 | (sut/render "Begin.\n {{=@ @=}}\nEnd.\n" {} {})))) 152 | (t/testing 153 | "Standalone Line Endings / \"\\r\\n\" should be considered a newline for standalone tags." 154 | (t/is (= "|\r\n|" (sut/render "|\r\n{{= @ @ =}}\r\n|" {} {})))) 155 | (t/testing 156 | "Standalone Without Previous Line / Standalone tags should not require a newline to precede them." 157 | (t/is (= "=" (sut/render " {{=@ @=}}\n=" {} {})))) 158 | (t/testing 159 | "Standalone Without Newline / Standalone tags should not require a newline to follow them." 160 | (t/is (= "=\n" (sut/render "=\n {{=@ @=}}" {} {})))) 161 | (t/testing 162 | "Pair with Padding / Superfluous in-tag whitespace should be ignored." 163 | (t/is (= "||" (sut/render "|{{= @ @ =}}|" {} {}))))) 164 | 165 | ;; (h/generate-test-cases-from-spec interpolation) 166 | 167 | (t/deftest interpolation-test 168 | (t/testing 169 | "No Interpolation / Mustache-free templates should render as-is." 170 | (t/is 171 | (= 172 | "Hello from {Mustache}!\n" 173 | (sut/render "Hello from {Mustache}!\n" {} {})))) 174 | (t/testing 175 | "Basic Interpolation / Unadorned tags should interpolate content into the template." 176 | (t/is 177 | (= 178 | "Hello, world!\n" 179 | (sut/render "Hello, {{subject}}!\n" {:subject "world"} {})))) 180 | (t/testing 181 | "HTML Escaping / Basic interpolation should be HTML escaped." 182 | (t/is 183 | (= 184 | "These characters should be HTML escaped: & " < >\n" 185 | (sut/render 186 | "These characters should be HTML escaped: {{forbidden}}\n" 187 | {:forbidden "& \" < >"} 188 | {})))) 189 | (t/testing 190 | "Triple Mustache / Triple mustaches should interpolate without HTML escaping." 191 | (t/is 192 | (= 193 | "These characters should not be HTML escaped: & \" < >\n" 194 | (sut/render 195 | "These characters should not be HTML escaped: {{{forbidden}}}\n" 196 | {:forbidden "& \" < >"} 197 | {})))) 198 | (t/testing 199 | "Ampersand / Ampersand should interpolate without HTML escaping." 200 | (t/is 201 | (= 202 | "These characters should not be HTML escaped: & \" < >\n" 203 | (sut/render 204 | "These characters should not be HTML escaped: {{&forbidden}}\n" 205 | {:forbidden "& \" < >"} 206 | {})))) 207 | (t/testing 208 | "Basic Integer Interpolation / Integers should interpolate seamlessly." 209 | (t/is 210 | (= 211 | "\"85 miles an hour!\"" 212 | (sut/render "\"{{mph}} miles an hour!\"" {:mph 85} {})))) 213 | (t/testing 214 | "Triple Mustache Integer Interpolation / Integers should interpolate seamlessly." 215 | (t/is 216 | (= 217 | "\"85 miles an hour!\"" 218 | (sut/render "\"{{{mph}}} miles an hour!\"" {:mph 85} {})))) 219 | (t/testing 220 | "Ampersand Integer Interpolation / Integers should interpolate seamlessly." 221 | (t/is 222 | (= 223 | "\"85 miles an hour!\"" 224 | (sut/render "\"{{&mph}} miles an hour!\"" {:mph 85} {})))) 225 | (t/testing 226 | "Basic Decimal Interpolation / Decimals should interpolate seamlessly with proper significance." 227 | (t/is 228 | (= 229 | "\"1.21 jiggawatts!\"" 230 | (sut/render "\"{{power}} jiggawatts!\"" {:power 1.21} {})))) 231 | (t/testing 232 | "Triple Mustache Decimal Interpolation / Decimals should interpolate seamlessly with proper significance." 233 | (t/is 234 | (= 235 | "\"1.21 jiggawatts!\"" 236 | (sut/render "\"{{{power}}} jiggawatts!\"" {:power 1.21} {})))) 237 | (t/testing 238 | "Ampersand Decimal Interpolation / Decimals should interpolate seamlessly with proper significance." 239 | (t/is 240 | (= 241 | "\"1.21 jiggawatts!\"" 242 | (sut/render "\"{{&power}} jiggawatts!\"" {:power 1.21} {})))) 243 | (t/testing 244 | "Basic Context Miss Interpolation / Failed context lookups should default to empty strings." 245 | (t/is 246 | (= 247 | "I () be seen!" 248 | (sut/render "I ({{cannot}}) be seen!" {} {})))) 249 | (t/testing 250 | "Triple Mustache Context Miss Interpolation / Failed context lookups should default to empty strings." 251 | (t/is 252 | (= 253 | "I () be seen!" 254 | (sut/render "I ({{{cannot}}}) be seen!" {} {})))) 255 | (t/testing 256 | "Ampersand Context Miss Interpolation / Failed context lookups should default to empty strings." 257 | (t/is 258 | (= 259 | "I () be seen!" 260 | (sut/render "I ({{&cannot}}) be seen!" {} {})))) 261 | (t/testing 262 | "Dotted Names - Basic Interpolation / Dotted names should be considered a form of shorthand for sections." 263 | (t/is 264 | (= 265 | "\"Joe\" == \"Joe\"" 266 | (sut/render 267 | "\"{{person.name}}\" == \"{{#person}}{{name}}{{/person}}\"" 268 | {:person {:name "Joe"}} 269 | {})))) 270 | (t/testing 271 | "Dotted Names - Triple Mustache Interpolation / Dotted names should be considered a form of shorthand for sections." 272 | (t/is 273 | (= 274 | "\"Joe\" == \"Joe\"" 275 | (sut/render 276 | "\"{{{person.name}}}\" == \"{{#person}}{{{name}}}{{/person}}\"" 277 | {:person {:name "Joe"}} 278 | {})))) 279 | (t/testing 280 | "Dotted Names - Ampersand Interpolation / Dotted names should be considered a form of shorthand for sections." 281 | (t/is 282 | (= 283 | "\"Joe\" == \"Joe\"" 284 | (sut/render 285 | "\"{{&person.name}}\" == \"{{#person}}{{&name}}{{/person}}\"" 286 | {:person {:name "Joe"}} 287 | {})))) 288 | (t/testing 289 | "Dotted Names - Arbitrary Depth / Dotted names should be functional to any level of nesting." 290 | (t/is 291 | (= 292 | "\"Phil\" == \"Phil\"" 293 | (sut/render 294 | "\"{{a.b.c.d.e.name}}\" == \"Phil\"" 295 | {:a {:b {:c {:d {:e {:name "Phil"}}}}}} 296 | {})))) 297 | (t/testing 298 | "Dotted Names - Broken Chains / Any falsey value prior to the last part of the name should yield ''." 299 | (t/is 300 | (= 301 | "\"\" == \"\"" 302 | (sut/render "\"{{a.b.c}}\" == \"\"" {:a {}} {})))) 303 | (t/testing 304 | "Dotted Names - Broken Chain Resolution / Each part of a dotted name should resolve only against its parent." 305 | (t/is 306 | (= 307 | "\"\" == \"\"" 308 | (sut/render 309 | "\"{{a.b.c.name}}\" == \"\"" 310 | {:a {:b {}}, :c {:name "Jim"}} 311 | {})))) 312 | (t/testing 313 | "Dotted Names - Initial Resolution / The first part of a dotted name should resolve as any other name." 314 | (t/is 315 | (= 316 | "\"Phil\" == \"Phil\"" 317 | (sut/render 318 | "\"{{#a}}{{b.c.d.e.name}}{{/a}}\" == \"Phil\"" 319 | {:a {:b {:c {:d {:e {:name "Phil"}}}}}, 320 | :b {:c {:d {:e {:name "Wrong"}}}}} 321 | {})))) 322 | (t/testing 323 | "Interpolation - Surrounding Whitespace / Interpolation should not alter surrounding whitespace." 324 | (t/is 325 | (= "| --- |" (sut/render "| {{string}} |" {:string "---"} {})))) 326 | (t/testing 327 | "Triple Mustache - Surrounding Whitespace / Interpolation should not alter surrounding whitespace." 328 | (t/is 329 | (= 330 | "| --- |" 331 | (sut/render "| {{{string}}} |" {:string "---"} {})))) 332 | (t/testing 333 | "Ampersand - Surrounding Whitespace / Interpolation should not alter surrounding whitespace." 334 | (t/is 335 | (= "| --- |" (sut/render "| {{&string}} |" {:string "---"} {})))) 336 | (t/testing 337 | "Interpolation - Standalone / Standalone interpolation should not alter surrounding whitespace." 338 | (t/is 339 | (= " ---\n" (sut/render " {{string}}\n" {:string "---"} {})))) 340 | (t/testing 341 | "Triple Mustache - Standalone / Standalone interpolation should not alter surrounding whitespace." 342 | (t/is 343 | (= 344 | " ---\n" 345 | (sut/render " {{{string}}}\n" {:string "---"} {})))) 346 | (t/testing 347 | "Ampersand - Standalone / Standalone interpolation should not alter surrounding whitespace." 348 | (t/is 349 | (= " ---\n" (sut/render " {{&string}}\n" {:string "---"} {})))) 350 | (t/testing 351 | "Interpolation With Padding / Superfluous in-tag whitespace should be ignored." 352 | (t/is 353 | (= "|---|" (sut/render "|{{ string }}|" {:string "---"} {})))) 354 | (t/testing 355 | "Triple Mustache With Padding / Superfluous in-tag whitespace should be ignored." 356 | (t/is 357 | (= "|---|" (sut/render "|{{{ string }}}|" {:string "---"} {})))) 358 | (t/testing 359 | "Ampersand With Padding / Superfluous in-tag whitespace should be ignored." 360 | (t/is 361 | (= "|---|" (sut/render "|{{& string }}|" {:string "---"} {}))))) 362 | 363 | ;; (h/generate-test-cases-from-spec inverted) 364 | 365 | (t/deftest inverted-test 366 | (t/testing 367 | "Falsey / Falsey sections should have their contents rendered." 368 | (t/is 369 | (= 370 | "\"This should be rendered.\"" 371 | (sut/render 372 | "\"{{^boolean}}This should be rendered.{{/boolean}}\"" 373 | {:boolean false} 374 | {})))) 375 | (t/testing 376 | "Truthy / Truthy sections should have their contents omitted." 377 | (t/is 378 | (= 379 | "\"\"" 380 | (sut/render 381 | "\"{{^boolean}}This should not be rendered.{{/boolean}}\"" 382 | {:boolean true} 383 | {})))) 384 | (t/testing 385 | "Context / Objects and hashes should behave like truthy values." 386 | (t/is 387 | (= 388 | "\"\"" 389 | (sut/render 390 | "\"{{^context}}Hi {{name}}.{{/context}}\"" 391 | {:context {:name "Joe"}} 392 | {})))) 393 | (t/testing 394 | "List / Lists should behave like truthy values." 395 | (t/is 396 | (= 397 | "\"\"" 398 | (sut/render 399 | "\"{{^list}}{{n}}{{/list}}\"" 400 | {:list [{:n 1} {:n 2} {:n 3}]} 401 | {})))) 402 | (t/testing 403 | "Empty List / Empty lists should behave like falsey values." 404 | (t/is 405 | (= 406 | "\"Yay lists!\"" 407 | (sut/render 408 | "\"{{^list}}Yay lists!{{/list}}\"" 409 | {:list []} 410 | {})))) 411 | (t/testing 412 | "Doubled / Multiple inverted sections per template should be permitted." 413 | (t/is 414 | (= 415 | "* first\n* second\n* third\n" 416 | (sut/render 417 | "{{^bool}}\n* first\n{{/bool}}\n* {{two}}\n{{^bool}}\n* third\n{{/bool}}\n" 418 | {:two "second", :bool false} 419 | {})))) 420 | (t/testing 421 | "Nested (Falsey) / Nested falsey sections should have their contents rendered." 422 | (t/is 423 | (= 424 | "| A B C D E |" 425 | (sut/render 426 | "| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |" 427 | {:bool false} 428 | {})))) 429 | (t/testing 430 | "Nested (Truthy) / Nested truthy sections should be omitted." 431 | (t/is 432 | (= 433 | "| A E |" 434 | (sut/render 435 | "| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |" 436 | {:bool true} 437 | {})))) 438 | (t/testing 439 | "Context Misses / Failed context lookups should be considered falsey." 440 | (t/is 441 | (= 442 | "[Cannot find key 'missing'!]" 443 | (sut/render 444 | "[{{^missing}}Cannot find key 'missing'!{{/missing}}]" 445 | {} 446 | {})))) 447 | (t/testing 448 | "Dotted Names - Truthy / Dotted names should be valid for Inverted Section tags." 449 | (t/is 450 | (= 451 | "\"\" == \"\"" 452 | (sut/render 453 | "\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"\"" 454 | {:a {:b {:c true}}} 455 | {})))) 456 | (t/testing 457 | "Dotted Names - Falsey / Dotted names should be valid for Inverted Section tags." 458 | (t/is 459 | (= 460 | "\"Not Here\" == \"Not Here\"" 461 | (sut/render 462 | "\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"" 463 | {:a {:b {:c false}}} 464 | {})))) 465 | (t/testing 466 | "Dotted Names - Broken Chains / Dotted names that cannot be resolved should be considered falsey." 467 | (t/is 468 | (= 469 | "\"Not Here\" == \"Not Here\"" 470 | (sut/render 471 | "\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"" 472 | {:a {}} 473 | {})))) 474 | (t/testing 475 | "Surrounding Whitespace / Inverted sections should not alter surrounding whitespace." 476 | (t/is 477 | (= 478 | " | \t|\t | \n" 479 | (sut/render 480 | " | {{^boolean}}\t|\t{{/boolean}} | \n" 481 | {:boolean false} 482 | {})))) 483 | (t/testing 484 | "Internal Whitespace / Inverted should not alter internal whitespace." 485 | (t/is 486 | (= 487 | " | \n | \n" 488 | (sut/render 489 | " | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n" 490 | {:boolean false} 491 | {})))) 492 | (t/testing 493 | "Indented Inline Sections / Single-line sections should not alter surrounding whitespace." 494 | (t/is 495 | (= 496 | " NO\n WAY\n" 497 | (sut/render 498 | " {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n" 499 | {:boolean false} 500 | {})))) 501 | (t/testing 502 | "Standalone Lines / Standalone lines should be removed from the template." 503 | (t/is 504 | (= 505 | "| This Is\n|\n| A Line\n" 506 | (sut/render 507 | "| This Is\n{{^boolean}}\n|\n{{/boolean}}\n| A Line\n" 508 | {:boolean false} 509 | {})))) 510 | (t/testing 511 | "Standalone Indented Lines / Standalone indented lines should be removed from the template." 512 | (t/is 513 | (= 514 | "| This Is\n|\n| A Line\n" 515 | (sut/render 516 | "| This Is\n {{^boolean}}\n|\n {{/boolean}}\n| A Line\n" 517 | {:boolean false} 518 | {})))) 519 | (t/testing 520 | "Standalone Line Endings / \"\\r\\n\" should be considered a newline for standalone tags." 521 | (t/is 522 | (= 523 | "|\r\n|" 524 | (sut/render 525 | "|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|" 526 | {:boolean false} 527 | {})))) 528 | (t/testing 529 | "Standalone Without Previous Line / Standalone tags should not require a newline to precede them." 530 | (t/is 531 | (= 532 | "^\n/" 533 | (sut/render 534 | " {{^boolean}}\n^{{/boolean}}\n/" 535 | {:boolean false} 536 | {})))) 537 | (t/testing 538 | "Standalone Without Newline / Standalone tags should not require a newline to follow them." 539 | (t/is 540 | (= 541 | "^\n/\n" 542 | (sut/render 543 | "^{{^boolean}}\n/\n {{/boolean}}" 544 | {:boolean false} 545 | {})))) 546 | (t/testing 547 | "Padding / Superfluous in-tag whitespace should be ignored." 548 | (t/is 549 | (= 550 | "|=|" 551 | (sut/render 552 | "|{{^ boolean }}={{/ boolean }}|" 553 | {:boolean false} 554 | {}))))) 555 | 556 | ;; (h/generate-test-cases-from-spec partials) 557 | 558 | (t/deftest partials-test 559 | (t/testing 560 | "Basic Behavior / The greater-than operator should expand to the named partial." 561 | (t/is 562 | (= 563 | "\"from partial\"" 564 | (sut/render 565 | "\"{{>text}}\"" 566 | {} 567 | {:partials {:text "from partial"}})))) 568 | (t/testing 569 | "Failed Lookup / The empty string should be used when the named partial is not found." 570 | (t/is (= "\"\"" (sut/render "\"{{>text}}\"" {} {:partials {}})))) 571 | (t/testing 572 | "Context / The greater-than operator should operate within the current context." 573 | (t/is 574 | (= 575 | "\"*content*\"" 576 | (sut/render 577 | "\"{{>partial}}\"" 578 | {:text "content"} 579 | {:partials {:partial "*{{text}}*"}})))) 580 | 581 | (comment 582 | "fix in the future" 583 | (t/testing 584 | "Recursion / The greater-than operator should properly recurse." 585 | (t/is 586 | (= 587 | "X>" 588 | (sut/render 589 | "{{>node}}" 590 | {:content "X", :nodes [{:content "Y", :nodes []}]} 591 | {:partials 592 | {:node "{{content}}<{{#nodes}}{{>node}}{{/nodes}}>"}}))))) 593 | 594 | (t/testing 595 | "Surrounding Whitespace / The greater-than operator should not alter surrounding whitespace." 596 | (t/is 597 | (= 598 | "| \t|\t |" 599 | (sut/render 600 | "| {{>partial}} |" 601 | {} 602 | {:partials {:partial "\t|\t"}})))) 603 | (t/testing 604 | "Inline Indentation / Whitespace should be left untouched." 605 | (t/is 606 | (= 607 | " | >\n>\n" 608 | (sut/render 609 | " {{data}} {{> partial}}\n" 610 | {:data "|"} 611 | {:partials {:partial ">\n>"}})))) 612 | (t/testing 613 | "Standalone Line Endings / \"\\r\\n\" should be considered a newline for standalone tags." 614 | (t/is 615 | (= 616 | "|\r\n>|" 617 | (sut/render 618 | "|\r\n{{>partial}}\r\n|" 619 | {} 620 | {:partials {:partial ">"}})))) 621 | (t/testing 622 | "Standalone Without Previous Line / Standalone tags should not require a newline to precede them." 623 | (t/is 624 | (= 625 | " >\n >>" 626 | (sut/render 627 | " {{>partial}}\n>" 628 | {} 629 | {:partials {:partial ">\n>"}})))) 630 | (t/testing 631 | "Standalone Without Newline / Standalone tags should not require a newline to follow them." 632 | (t/is 633 | (= 634 | ">\n >\n >" 635 | (sut/render 636 | ">\n {{>partial}}" 637 | {} 638 | {:partials {:partial ">\n>"}})))) 639 | (t/testing 640 | "Standalone Indentation / Each line of the partial should be indented before rendering." 641 | (t/is 642 | (= 643 | "\\\n |\n <\n->\n |\n/\n" 644 | (sut/render 645 | "\\\n {{>partial}}\n/\n" 646 | {:content "<\n->"} 647 | {:partials {:partial "|\n{{{content}}}\n|\n"}})))) 648 | (t/testing 649 | "Padding Whitespace / Superfluous in-tag whitespace should be ignored." 650 | (t/is 651 | (= 652 | "|[]|" 653 | (sut/render 654 | "|{{> partial }}|" 655 | {:boolean true} 656 | {:partials {:partial "[]"}}))))) 657 | 658 | ;; (h/generate-test-cases-from-spec sections) 659 | 660 | (t/deftest sections-test 661 | (t/testing 662 | "Truthy / Truthy sections should have their contents rendered." 663 | (t/is 664 | (= 665 | "\"This should be rendered.\"" 666 | (sut/render 667 | "\"{{#boolean}}This should be rendered.{{/boolean}}\"" 668 | {:boolean true} 669 | {})))) 670 | (t/testing 671 | "Falsey / Falsey sections should have their contents omitted." 672 | (t/is 673 | (= 674 | "\"\"" 675 | (sut/render 676 | "\"{{#boolean}}This should not be rendered.{{/boolean}}\"" 677 | {:boolean false} 678 | {})))) 679 | (t/testing 680 | "Context / Objects and hashes should be pushed onto the context stack." 681 | (t/is 682 | (= 683 | "\"Hi Joe.\"" 684 | (sut/render 685 | "\"{{#context}}Hi {{name}}.{{/context}}\"" 686 | {:context {:name "Joe"}} 687 | {})))) 688 | (t/testing 689 | "Deeply Nested Contexts / All elements on the context stack should be accessible." 690 | (t/is 691 | (= 692 | "1\n121\n12321\n1234321\n123454321\n1234321\n12321\n121\n1\n" 693 | (sut/render 694 | "{{#a}}\n{{one}}\n{{#b}}\n{{one}}{{two}}{{one}}\n{{#c}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{#d}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{#e}}\n{{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}\n{{/e}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{/d}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{/c}}\n{{one}}{{two}}{{one}}\n{{/b}}\n{{one}}\n{{/a}}\n" 695 | {:a {:one 1}, 696 | :b {:two 2}, 697 | :c {:three 3}, 698 | :d {:four 4}, 699 | :e {:five 5}} 700 | {})))) 701 | (t/testing 702 | "List / Lists should be iterated; list items should visit the context stack." 703 | (t/is 704 | (= 705 | "\"123\"" 706 | (sut/render 707 | "\"{{#list}}{{item}}{{/list}}\"" 708 | {:list [{:item 1} {:item 2} {:item 3}]} 709 | {})))) 710 | (t/testing 711 | "Empty List / Empty lists should behave like falsey values." 712 | (t/is 713 | (= 714 | "\"\"" 715 | (sut/render 716 | "\"{{#list}}Yay lists!{{/list}}\"" 717 | {:list []} 718 | {})))) 719 | (t/testing 720 | "Doubled / Multiple sections per template should be permitted." 721 | (t/is 722 | (= 723 | "* first\n* second\n* third\n" 724 | (sut/render 725 | "{{#bool}}\n* first\n{{/bool}}\n* {{two}}\n{{#bool}}\n* third\n{{/bool}}\n" 726 | {:two "second", :bool true} 727 | {})))) 728 | (t/testing 729 | "Nested (Truthy) / Nested truthy sections should have their contents rendered." 730 | (t/is 731 | (= 732 | "| A B C D E |" 733 | (sut/render 734 | "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |" 735 | {:bool true} 736 | {})))) 737 | (t/testing 738 | "Nested (Falsey) / Nested falsey sections should be omitted." 739 | (t/is 740 | (= 741 | "| A E |" 742 | (sut/render 743 | "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |" 744 | {:bool false} 745 | {})))) 746 | (t/testing 747 | "Context Misses / Failed context lookups should be considered falsey." 748 | (t/is 749 | (= 750 | "[]" 751 | (sut/render 752 | "[{{#missing}}Found key 'missing'!{{/missing}}]" 753 | {} 754 | {})))) 755 | (t/testing 756 | "Implicit Iterator - String / Implicit iterators should directly interpolate strings." 757 | (t/is 758 | (= 759 | "\"(a)(b)(c)(d)(e)\"" 760 | (sut/render 761 | "\"{{#list}}({{.}}){{/list}}\"" 762 | {:list ["a" "b" "c" "d" "e"]} 763 | {})))) 764 | (t/testing 765 | "Implicit Iterator - Integer / Implicit iterators should cast integers to strings and interpolate." 766 | (t/is 767 | (= 768 | "\"(1)(2)(3)(4)(5)\"" 769 | (sut/render 770 | "\"{{#list}}({{.}}){{/list}}\"" 771 | {:list [1 2 3 4 5]} 772 | {})))) 773 | (t/testing 774 | "Implicit Iterator - Decimal / Implicit iterators should cast decimals to strings and interpolate." 775 | (t/is 776 | (= 777 | "\"(1.1)(2.2)(3.3)(4.4)(5.5)\"" 778 | (sut/render 779 | "\"{{#list}}({{.}}){{/list}}\"" 780 | {:list [1.1 2.2 3.3 4.4 5.5]} 781 | {})))) 782 | (t/testing 783 | "Implicit Iterator - Array / Implicit iterators should allow iterating over nested arrays." 784 | (t/is 785 | (= 786 | "\"(123)(abc)\"" 787 | (sut/render 788 | "\"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}\"" 789 | {:list [[1 2 3] ["a" "b" "c"]]} 790 | {})))) 791 | (t/testing 792 | "Dotted Names - Truthy / Dotted names should be valid for Section tags." 793 | (t/is 794 | (= 795 | "\"Here\" == \"Here\"" 796 | (sut/render 797 | "\"{{#a.b.c}}Here{{/a.b.c}}\" == \"Here\"" 798 | {:a {:b {:c true}}} 799 | {})))) 800 | (t/testing 801 | "Dotted Names - Falsey / Dotted names should be valid for Section tags." 802 | (t/is 803 | (= 804 | "\"\" == \"\"" 805 | (sut/render 806 | "\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"" 807 | {:a {:b {:c false}}} 808 | {})))) 809 | (t/testing 810 | "Dotted Names - Broken Chains / Dotted names that cannot be resolved should be considered falsey." 811 | (t/is 812 | (= 813 | "\"\" == \"\"" 814 | (sut/render 815 | "\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"" 816 | {:a {}} 817 | {})))) 818 | (t/testing 819 | "Surrounding Whitespace / Sections should not alter surrounding whitespace." 820 | (t/is 821 | (= 822 | " | \t|\t | \n" 823 | (sut/render 824 | " | {{#boolean}}\t|\t{{/boolean}} | \n" 825 | {:boolean true} 826 | {})))) 827 | (t/testing 828 | "Internal Whitespace / Sections should not alter internal whitespace." 829 | (t/is 830 | (= 831 | " | \n | \n" 832 | (sut/render 833 | " | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n" 834 | {:boolean true} 835 | {})))) 836 | (t/testing 837 | "Indented Inline Sections / Single-line sections should not alter surrounding whitespace." 838 | (t/is 839 | (= 840 | " YES\n GOOD\n" 841 | (sut/render 842 | " {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n" 843 | {:boolean true} 844 | {})))) 845 | (t/testing 846 | "Standalone Lines / Standalone lines should be removed from the template." 847 | (t/is 848 | (= 849 | "| This Is\n|\n| A Line\n" 850 | (sut/render 851 | "| This Is\n{{#boolean}}\n|\n{{/boolean}}\n| A Line\n" 852 | {:boolean true} 853 | {})))) 854 | (t/testing 855 | "Indented Standalone Lines / Indented standalone lines should be removed from the template." 856 | (t/is 857 | (= 858 | "| This Is\n|\n| A Line\n" 859 | (sut/render 860 | "| This Is\n {{#boolean}}\n|\n {{/boolean}}\n| A Line\n" 861 | {:boolean true} 862 | {})))) 863 | (t/testing 864 | "Standalone Line Endings / \"\\r\\n\" should be considered a newline for standalone tags." 865 | (t/is 866 | (= 867 | "|\r\n|" 868 | (sut/render 869 | "|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|" 870 | {:boolean true} 871 | {})))) 872 | (t/testing 873 | "Standalone Without Previous Line / Standalone tags should not require a newline to precede them." 874 | (t/is 875 | (= 876 | "#\n/" 877 | (sut/render 878 | " {{#boolean}}\n#{{/boolean}}\n/" 879 | {:boolean true} 880 | {})))) 881 | (t/testing 882 | "Standalone Without Newline / Standalone tags should not require a newline to follow them." 883 | (t/is 884 | (= 885 | "#\n/\n" 886 | (sut/render 887 | "#{{#boolean}}\n/\n {{/boolean}}" 888 | {:boolean true} 889 | {})))) 890 | (t/testing 891 | "Padding / Superfluous in-tag whitespace should be ignored." 892 | (t/is 893 | (= 894 | "|=|" 895 | (sut/render 896 | "|{{# boolean }}={{/ boolean }}|" 897 | {:boolean true} 898 | {}))))) 899 | 900 | ;; (h/generate-test-cases-from-spec %7Elambdas) 901 | 902 | (t/deftest lambdas-test 903 | (t/testing 904 | "Interpolation / A lambda's return value should be interpolated." 905 | (t/is 906 | (= 907 | "Hello, world!" 908 | (sut/render 909 | "Hello, {{lambda}}!" 910 | {:lambda (fn [] "world")} 911 | {})))) 912 | 913 | (t/testing 914 | "Interpolation - Expansion / A lambda's return value should be parsed." 915 | (t/is 916 | (= 917 | "Hello, world!" 918 | (sut/render 919 | "Hello, {{lambda}}!" 920 | {:planet "world", 921 | :lambda (fn [] "{{planet}}")} 922 | {})))) 923 | 924 | (t/testing 925 | "Interpolation - Alternate Delimiters / A lambda's return value should parse with the default delimiters." 926 | (t/is 927 | (= 928 | "Hello, (|planet| => world)!" 929 | (sut/render 930 | "{{= | | =}}\nHello, (|&lambda|)!" 931 | {:planet "world", 932 | :lambda (fn [] "|planet| => {{planet}}")} 933 | {})))) 934 | 935 | (t/testing 936 | "Interpolation - Multiple Calls / Interpolated lambdas should not be cached." 937 | (t/is 938 | (= 939 | "1 == 2 == 3" 940 | (sut/render 941 | "{{lambda}} == {{{lambda}}} == {{lambda}}" 942 | {:lambda (let [g (atom 0)] (fn [] (swap! g inc)))} 943 | {})))) 944 | 945 | (t/testing 946 | "Escaping / Lambda results should be appropriately escaped." 947 | (t/is 948 | (= 949 | "<>>" 950 | (sut/render 951 | "<{{lambda}}{{{lambda}}}" 952 | {:lambda (fn [] ">")} 953 | {})))) 954 | 955 | (t/testing 956 | "Section / Lambdas used for sections should receive the raw section string." 957 | (t/is 958 | (= 959 | "" 960 | (sut/render 961 | "<{{#lambda}}{{x}}{{/lambda}}>" 962 | {:x "Error!", 963 | :lambda (fn [text] (if (= text "{{x}}") "yes" "no"))} 964 | {})))) 965 | 966 | (t/testing 967 | "Section - Expansion / Lambdas used for sections should have their results parsed." 968 | (t/is 969 | (= 970 | "<-Earth->" 971 | (sut/render 972 | "<{{#lambda}}-{{/lambda}}>" 973 | {:planet "Earth", 974 | :lambda (fn [text] (str text "{{planet}}" text))} 975 | {})))) 976 | 977 | (t/testing 978 | "Section - Alternate Delimiters / Lambdas used for sections should parse with the current delimiters." 979 | (t/is 980 | (= 981 | "<-{{planet}} => Earth->" 982 | (sut/render 983 | "{{= | | =}}<|#lambda|-|/lambda|>" 984 | {:planet "Earth", 985 | :lambda (fn [text] (str text "{{planet}} => |planet|" text))} 986 | {})))) 987 | 988 | (t/testing 989 | "Section - Multiple Calls / Lambdas used for sections should not be cached." 990 | (t/is 991 | (= 992 | "__FILE__ != __LINE__" 993 | (sut/render 994 | "{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}" 995 | {:lambda (fn [text] (str "__" text "__"))} 996 | {})))) 997 | 998 | (t/testing 999 | "Inverted Section / Lambdas used for inverted sections should be considered truthy." 1000 | (t/is 1001 | (= 1002 | "<>" 1003 | (sut/render 1004 | "<{{^lambda}}{{static}}{{/lambda}}>" 1005 | {:static "static", 1006 | :lambda (fn [text] false)} 1007 | {}))))) 1008 | --------------------------------------------------------------------------------