From 2f8825951ca91a5080fb58d368c9b910276a3d8c Mon Sep 17 00:00:00 2001 From: "amit.gold" Date: Wed, 10 Jun 2026 14:43:03 +0300 Subject: [PATCH 1/2] Add *escape-fn* variable --- src/selmer/filter_parser.clj | 15 +++++++++------ src/selmer/util.clj | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/selmer/filter_parser.clj b/src/selmer/filter_parser.clj index 100d2b8..a26b9c8 100644 --- a/src/selmer/filter_parser.clj +++ b/src/selmer/filter_parser.clj @@ -14,17 +14,18 @@ arguments." (:require [selmer.filters :refer [get-filter]] - [selmer.util :refer [*escape-variables* parse-accessor]] + [selmer.util :refer [*escape-variables* *escape-fn* parse-accessor]] [clojure.string :as str])) ;;; More Utils (defn escape-html* "HTML-escapes the given string. Escapes the same characters as django's escape." - [^String s] + [input] ;; This method is "Java in Clojure" for serious speedups. ;; Stolen from davidsantiago/quoin and modified. (if *escape-variables* - (let [slength (count s) + (let [s (str input) + slength (count s) sb (StringBuilder. slength)] (loop [idx 0] (if (>= idx slength) @@ -38,7 +39,9 @@ \' (.append sb "'") (.append sb c)) (recur (inc idx)))))) - s)) + (str input))) + + (defn strip-doublequotes "Removes doublequotes from the start and end of a string if any." @@ -58,8 +61,8 @@ (if (and (vector? x) (= :safe (first x))) (second x) - (let [s (str x)] - (escape-html* s)))) + (let [escape-fn (or *escape-fn* escape-html*)] + (escape-fn x)))) ;;; Compile filters diff --git a/src/selmer/util.clj b/src/selmer/util.clj index 8ffcc66..f19af6b 100644 --- a/src/selmer/util.clj +++ b/src/selmer/util.clj @@ -28,6 +28,7 @@ (set! *url-stream-handler* path))) (def ^:dynamic *escape-variables* true) +(def ^:dynamic *escape-fn* nil) (defn turn-off-escaping! [] (alter-var-root #'*escape-variables* From ff87dfafc34d64bb03519b6c67411f8d7ce12ad4 Mon Sep 17 00:00:00 2001 From: "amit.gold" Date: Mon, 15 Jun 2026 15:15:13 +0300 Subject: [PATCH 2/2] Move escape-html, add test --- src/selmer/filter_parser.clj | 29 +++-------------------------- src/selmer/tags.clj | 2 +- src/selmer/util.clj | 26 +++++++++++++++++++++++++- test/selmer/core_test.clj | 22 ++++++++++++++++++++++ 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/selmer/filter_parser.clj b/src/selmer/filter_parser.clj index a26b9c8..a4a9b0a 100644 --- a/src/selmer/filter_parser.clj +++ b/src/selmer/filter_parser.clj @@ -18,30 +18,6 @@ [clojure.string :as str])) ;;; More Utils -(defn escape-html* - "HTML-escapes the given string. Escapes the same characters as django's escape." - [input] - ;; This method is "Java in Clojure" for serious speedups. - ;; Stolen from davidsantiago/quoin and modified. - (if *escape-variables* - (let [s (str input) - slength (count s) - sb (StringBuilder. slength)] - (loop [idx 0] - (if (>= idx slength) - (.toString sb) - (let [c (char (.charAt s idx))] - (case c - \& (.append sb "&") - \< (.append sb "<") - \> (.append sb ">") - \" (.append sb """) - \' (.append sb "'") - (.append sb c)) - (recur (inc idx)))))) - (str input))) - - (defn strip-doublequotes "Removes doublequotes from the start and end of a string if any." @@ -61,8 +37,9 @@ (if (and (vector? x) (= :safe (first x))) (second x) - (let [escape-fn (or *escape-fn* escape-html*)] - (escape-fn x)))) + (if *escape-variables* + (*escape-fn* x) + (str x)))) ;;; Compile filters diff --git a/src/selmer/tags.clj b/src/selmer/tags.clj index 8a413f0..c2ffd35 100644 --- a/src/selmer/tags.clj +++ b/src/selmer/tags.clj @@ -3,7 +3,7 @@ clojure.java.io [clojure.string :as str] selmer.node - [selmer.filter-parser :refer [literal? parse-literal safe-filter compile-filter-body get-accessor escape-html*]] + [selmer.filter-parser :refer [literal? parse-literal safe-filter compile-filter-body get-accessor]] [selmer.filters :refer [filters]] [selmer.util :refer :all]) (:import [selmer.node TextNode])) diff --git a/src/selmer/util.clj b/src/selmer/util.clj index f19af6b..eef8141 100644 --- a/src/selmer/util.clj +++ b/src/selmer/util.clj @@ -28,7 +28,31 @@ (set! *url-stream-handler* path))) (def ^:dynamic *escape-variables* true) -(def ^:dynamic *escape-fn* nil) + +(defn escape-html* + "HTML-escapes the given string. Escapes the same characters as django's escape." + [input] + ;; This method is "Java in Clojure" for serious speedups. + ;; Stolen from davidsantiago/quoin and modified. + (if *escape-variables* + (let [s (str input) + slength (count s) + sb (StringBuilder. slength)] + (loop [idx 0] + (if (>= idx slength) + (.toString sb) + (let [c (char (.charAt s idx))] + (case c + \& (.append sb "&") + \< (.append sb "<") + \> (.append sb ">") + \" (.append sb """) + \' (.append sb "'") + (.append sb c)) + (recur (inc idx)))))) + (str input))) + +(def ^:dynamic *escape-fn* escape-html*) (defn turn-off-escaping! [] (alter-var-root #'*escape-variables* diff --git a/test/selmer/core_test.clj b/test/selmer/core_test.clj index 67314ee..544efe9 100644 --- a/test/selmer/core_test.clj +++ b/test/selmer/core_test.clj @@ -1364,6 +1364,28 @@ (is (= "I <3 ponies" (render "{{name}}" {:name "I <3 ponies"}))) (finally (turn-on-escaping!))))) +(deftest escape-fn-test + (testing "default escaping coerces non-string values to strings" + (is (= "123" (render "{{n}}" {:n 123}))) + (is (= "true" (render "{{b}}" {:b true})))) + + (testing "*escape-fn* overrides the default escaping function" + (binding [*escape-fn* str/upper-case] + (is (= "I <3 PONIES" (render "{{name}}" {:name "i <3 ponies"})))) + ;; ensure default escaping resumes outside the binding + (is (= "I <3 ponies" (render "{{name}}" {:name "I <3 ponies"})))) + + (testing "*escape-fn* is not applied to values marked safe" + (binding [*escape-fn* str/upper-case] + (is (= "I <3 ponies" (render "{{name|safe}}" {:name "I <3 ponies"}))))) + + (testing "*escape-fn* is bypassed when escaping is turned off" + (try + (turn-off-escaping!) + (binding [*escape-fn* str/upper-case] + (is (= "I <3 ponies" (render "{{name}}" {:name "I <3 ponies"})))) + (finally (turn-on-escaping!))))) + (deftest name-test (testing "converts keywords to strings" (is (= "foobar" (render "{{foo|name}}" {:foo :foobar})))