├── deps.edn ├── .gitignore ├── project.clj ├── test └── comb │ └── test │ └── template.clj ├── .github └── workflows │ └── test.yml ├── src └── comb │ └── template.clj └── README.md /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.9.0"}}} 2 | -------------------------------------------------------------------------------- /.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 | .clj-kondo 13 | .cpcache 14 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject comb "1.0.0" 2 | :description "Clojure templating library similar to ERB" 3 | :url "https://github.com/weavejester/comb" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.9.0"]]) 7 | -------------------------------------------------------------------------------- /test/comb/test/template.clj: -------------------------------------------------------------------------------- 1 | (ns comb.test.template 2 | (:require [clojure.test :refer [deftest is]] 3 | [comb.template :as t])) 4 | 5 | (deftest eval-test 6 | (is (= (t/eval "foo") "foo")) 7 | (is (= (t/eval "<%= 10 %>") "10")) 8 | (is (= (t/eval "<%= x %>" {:x "foo"}) "foo")) 9 | (is (= (t/eval "<%=x%>" {:x "foo"}) "foo")) 10 | (is (= (t/eval "<% (doseq [x xs] %>foo<%= x %> <% ) %>" {:xs [1 2 3]}) 11 | "foo1 foo2 foo3 "))) 12 | 13 | (deftest fn-test 14 | (is (= ((t/fn [x] "foo<%= x %>") "bar") 15 | "foobar"))) 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v5 9 | 10 | - name: Prepare java 11 | uses: actions/setup-java@v5 12 | with: 13 | distribution: 'zulu' 14 | java-version: '11' 15 | 16 | - name: Install clojure tools 17 | uses: DeLaGuardo/setup-clojure@13.4 18 | with: 19 | lein: 2.12.0 20 | 21 | - name: Cache clojure dependencies 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.m2/repository 25 | key: cljdeps-${{ hashFiles('project.clj') }} 26 | restore-keys: cljdeps- 27 | 28 | - name: Run tests 29 | run: lein test 30 | -------------------------------------------------------------------------------- /src/comb/template.clj: -------------------------------------------------------------------------------- 1 | (ns comb.template 2 | "Clojure templating library." 3 | (:refer-clojure :exclude [fn eval]) 4 | (:require [clojure.core :as core])) 5 | 6 | (defn- read-source [source] 7 | (if (string? source) 8 | source 9 | (slurp source))) 10 | 11 | (def delimiters ["<%" "%>"]) 12 | 13 | (def parser-regex 14 | (re-pattern 15 | (str "(?s)\\A" 16 | "(?:" "(.*?)" 17 | (first delimiters) "(.*?)" (last delimiters) 18 | ")?" 19 | "(.*)\\z"))) 20 | 21 | (defn emit-string [s] 22 | (print "(print " (pr-str s) ")")) 23 | 24 | (defn emit-expr [expr] 25 | (if (.startsWith expr "=") 26 | (print "(print " (subs expr 1) ")") 27 | (print expr))) 28 | 29 | (defn- parse-string [src] 30 | (with-out-str 31 | (print "(do ") 32 | (loop [src src] 33 | (let [[_ before expr after] (re-matches parser-regex src)] 34 | (if expr 35 | (do (emit-string before) 36 | (emit-expr expr) 37 | (recur after)) 38 | (do (emit-string after) 39 | (print ")"))))))) 40 | 41 | (defn compile-fn [args src] 42 | (core/eval 43 | `(core/fn ~args 44 | (with-out-str 45 | ~(-> src read-source parse-string read-string))))) 46 | 47 | (defmacro fn 48 | "Compile a template into a function that takes the supplied arguments. The 49 | template source may be a string, or an I/O source such as a File, Reader or 50 | InputStream." 51 | {:clj-kondo/lint-as 'clojure.core/fn 52 | :clj-kondo/ignore [:unused-binding]} 53 | [args source] 54 | `(compile-fn '~args ~source)) 55 | 56 | (defn eval 57 | "Evaluate a template using the supplied bindings. The template source may 58 | be a string, or an I/O source such as a File, Reader or InputStream." 59 | ([source] 60 | (eval source {})) 61 | ([source bindings] 62 | (let [keys (map (comp symbol name) (keys bindings)) 63 | func (compile-fn [{:keys (vec keys)}] source)] 64 | (func bindings)))) 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Comb [![Build Status](https://github.com/weavejester/comb/actions/workflows/test.yml/badge.svg)](https://github.com/weavejester/comb/actions/workflows/test.yml) 2 | 3 | Comb is a simple templating system for Clojure. You can use Comb to embed 4 | fragments of Clojure code into a text file. 5 | 6 | ## Installation 7 | 8 | Add the following dependency to your deps.edn file: 9 | 10 | comb/comb {:mvn/version "1.0.0"} 11 | 12 | Or to your Leiningen project file: 13 | 14 | [comb "1.0.0"] 15 | 16 | ## Syntax 17 | 18 | The `<% %>` tags are used to embed a section of Clojure code with side-effects. 19 | This is commonly used for control structures like loops or conditionals. 20 | 21 | For example: 22 | 23 | ```clojure 24 | (require '[comb.template :as template]) 25 | 26 | (template/eval "<% (dotimes [x 3] %>foo<% ) %>") 27 | => "foofoofoo" 28 | ``` 29 | 30 | The `<%= %>` tags will be subsituted for the value of the expression within them. 31 | This is used for inserting values into a template. 32 | 33 | For example: 34 | 35 | ```clojure 36 | (template/eval "Hello <%= name %>" {:name "Alice"}) 37 | => "Hello Alice" 38 | ``` 39 | 40 | ## API Documentation 41 | 42 | ### template/eval 43 | 44 | ```clojure 45 | (template/eval source) 46 | (template/eval source bindings) 47 | ``` 48 | 49 | Evaluate a template source using an optional map of bindings. The template 50 | source can be a string, or any I/O source understood by the standard `slurp` 51 | function. 52 | 53 | Example of use: 54 | 55 | ```clojure 56 | (template/eval "Hello <%= name %>" {:name "Bob"}) 57 | ``` 58 | 59 | ### template/fn 60 | 61 | ```clojure 62 | (template/fn args source) 63 | ``` 64 | 65 | Compile a template source into a anonymous function. This is a lot faster 66 | than `template/eval` for repeated calls, as the template source is only 67 | parsed when the function is created. 68 | 69 | Example of use: 70 | 71 | ```clojure 72 | (def hello 73 | (template/fn [name] "Hello <%= name %>")) 74 | 75 | (hello "Alice") 76 | ``` 77 | 78 | ## License 79 | 80 | Copyright © 2025 James Reeves 81 | 82 | Distributed under the Eclipse Public License either version 1.0 or (at 83 | your option) any later version. 84 | --------------------------------------------------------------------------------