Skip to content

Commit 25b2b8f

Browse files
authored
add namespacing for script tag keys (#326)
* add namespacing for script tag keys * improve deprecation warning tests
1 parent 3793751 commit 25b2b8f

6 files changed

Lines changed: 176 additions & 17 deletions

File tree

README.md

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,31 +1043,52 @@ Note, the escaping of variables can also be controlled through the dynamic bindi
10431043

10441044
#### script
10451045

1046-
The script tag will generate an HTML script tag and prepend the value of the `selmer/context` key
1047-
to the URI. When `selmer/context` key is not present then the original URI is set.
1046+
The script tag will generate an HTML script tag and prepend the value of the `:selmer/context` key
1047+
to the URI. When `:selmer/context` key is not present then the original URI is set.
10481048

10491049
`(render "{% script \"/js/site.js\" %}" {:selmer/context "/myapp"})` =>
10501050
```
1051-
"<script async-attr defer-attr src=\"/myapp/js/site.js\" type=\"text/javascript\"></script>"
1051+
"<script src=\"/myapp/js/site.js\" type=\"application/javascript\"></script>"
10521052
```
10531053

1054-
When `type` key is present its value is used for the 'type' attribute.
1054+
The tag supports the following optional arguments:
1055+
1056+
* `type` - sets the `type` attribute (default: `application/javascript`)
1057+
* `async` - when truthy, adds the `async` attribute
1058+
* `defer` - when truthy, adds the `defer` attribute
10551059

1056-
`(render "{% script \"/js/site.js\" type=\"module\" %}" ` =>
1060+
`(render "{% script \"/js/site.js\" type=\"module\" %}" {})` =>
10571061
```
10581062
"<script src=\"/js/site.js\" type=\"module\"></script>"
10591063
```
10601064

1065+
`(render "{% script \"/js/site.js\" async=\"true\" defer=\"true\" %}" {})` =>
1066+
```
1067+
"<script async defer src=\"/js/site.js\" type=\"application/javascript\"></script>"
1068+
```
1069+
1070+
These attributes can also be set via the context map using namespaced keys:
1071+
`:selmer/type`, `:selmer/async`, and `:selmer/defer`.
1072+
1073+
`(render "{% script \"/js/site.js\" %}" {:selmer/type "module" :selmer/async true})` =>
1074+
```
1075+
"<script async src=\"/js/site.js\" type=\"module\"></script>"
1076+
```
1077+
1078+
**Note (since 1.13.0):** The `:type` context key is no longer consumed by the script tag — use
1079+
`:selmer/type` instead. The non-namespaced `:async` and `:defer` context keys are deprecated;
1080+
use `:selmer/async` and `:selmer/defer` to avoid conflicts with your own context data.
1081+
10611082
Since 1.11.1 URI can be a name of context parameter with optional filters.
10621083

10631084
`(render "{% script path %}" {:selmer/context "/myapp" :path "/js/site.js"})` =>
10641085
```
1065-
"<script async-attr defer-attr src=\"/myapp/js/site.js\" type=\"text/javascript\"></script>"
1086+
"<script src=\"/myapp/js/site.js\" type=\"application/javascript\"></script>"
10661087
```
10671088

10681089
`(render "{% script path|upper %}" {:selmer/context "/myapp" :path "/js/site.js"})` =>
10691090
```
1070-
"<script async-attr defer-attr src=\"/myapp/JS/SITE.JS\" type=\"text/javascript\"></script>"
1091+
"<script src=\"/myapp/JS/SITE.JS\" type=\"application/javascript\"></script>"
10711092
```
10721093
#### style
10731094

changes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
* 1.13.0 - [namespace script tag context keys to avoid collisions](https://github.com/yogthos/Selmer/issues/325): `script` tag now uses `:selmer/type`, `:selmer/async`, and `:selmer/defer` context keys; non-namespaced `:async` and `:defer` are deprecated with warnings, `:type` requires `:selmer/type` - **breaking change**
2+
13
* 1.12.70 - [add `get` filter](https://github.com/yogthos/Selmer/issue/322)
24
* 1.12.69 - fix relfection warning
35
* 1.12.68 - fix handling nulls when parsing script tags

project.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(defproject selmer "1.12.70"
1+
(defproject selmer "1.13.0"
22
:description "Django style templates for Clojure"
33
:url "https://github.com/yogthos/Selmer"
44
:license {:name "Eclipse Public License"

src/selmer/tags.clj

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,15 @@
315315
(for [[id value] (partition 2 args)]
316316
[(map keyword (str/split id #"\.")) (compile-filter-body value false)]))
317317

318+
(defn- compile-args-namespaced
319+
"Like compile-args but namespaces keys under selmer/ to avoid conflicts with user context."
320+
[args]
321+
(when-not (even? (count args))
322+
(throw (ex-info (str "invalid arguments passed to tag: " args)
323+
{:args args})))
324+
(for [[id value] (partition 2 args)]
325+
[(map #(keyword "selmer" %) (str/split id #"\.")) (compile-filter-body value false)]))
326+
318327
(defn with-handler [args tag-content render rdr]
319328
(let [content (get-in (tag-content rdr :with :endwith) [:with :content])
320329
args (->> args
@@ -355,27 +364,38 @@
355364
number of optional arguments. Value for 'src' attribute is built accounting
356365
value of `selmer/context` context parameter and `uri` can be a string literal
357366
or name of context parameter (filters also supported). Optional arguments are:
358-
* `async` - when evaluates to logical true then 'async' attribute would be
359-
added to generated tag.
360-
* `defer` - when evaluates to logical true then 'defer' attribute would be
361-
added to generated tag.
362-
* `type` - when present its value is used for the 'type' attribute."
367+
* `selmer/async` (or deprecated `async`) - when evaluates to logical true
368+
then 'async' attribute would be added to generated tag.
369+
* `selmer/defer` (or deprecated `defer`) - when evaluates to logical true
370+
then 'defer' attribute would be added to generated tag.
371+
* `selmer/type` - when present its value is used for the 'type' attribute.
372+
373+
Note: Using non-namespaced keys `:async` and `:defer` in the context map is
374+
deprecated. Please use `:selmer/async` and `:selmer/defer` instead to avoid
375+
conflicts with your own context data. The `:type` key is no longer supported
376+
in the context map; use `:selmer/type` instead."
363377
[[^String uri & args] _ _ _]
364378
(let [args
365379
(->> args
366380
(mapcat #(.split ^String % "="))
367381
(remove #{"="})
368-
(compile-args))]
382+
(compile-args-namespaced))]
369383
(fn [context-map]
370384
(let [args
371385
(reduce
372386
(fn [context-map [k v]]
373387
(assoc-in context-map k (v context-map)))
374388
context-map
375389
args)
376-
async-attr (when (:async args) "async ")
377-
defer-attr (when (:defer args) "defer ")
378-
type-attr (or (:type args) "application/javascript")
390+
async-attr (when (if (contains? args :selmer/async)
391+
(:selmer/async args)
392+
(deprecated-key-lookup args :selmer/async :async))
393+
"async ")
394+
defer-attr (when (if (contains? args :selmer/defer)
395+
(:selmer/defer args)
396+
(deprecated-key-lookup args :selmer/defer :defer))
397+
"defer ")
398+
type-attr (or (:selmer/type args) "application/javascript")
379399
src-attr-val (build-uri-for-script-or-style-tag uri context-map)]
380400
(str "<script " async-attr defer-attr "src=\"" src-attr-val "\" type=\"" type-attr "\"></script>")))))
381401

src/selmer/util.clj

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,41 @@ so it can access vectors as well as maps."
294294
algo (MessageDigest/getInstance algo)
295295
bs (.digest algo (.getBytes s))]
296296
(format "%032x" (BigInteger. 1 bs))))
297+
298+
;; Deprecation warning infrastructure
299+
(def ^:dynamic *warn-on-deprecated-keys* true)
300+
301+
(defonce ^:private warned-keys (atom #{}))
302+
303+
(defn- default-deprecation-warning-handler
304+
"Default handler that tries clojure.tools.logging first, falls back to stderr."
305+
[message]
306+
(try
307+
(require 'clojure.tools.logging)
308+
(require 'clojure.tools.logging.impl)
309+
(let [get-logger (resolve 'clojure.tools.logging.impl/get-logger)
310+
factory @(resolve 'clojure.tools.logging/*logger-factory*)
311+
logger (get-logger factory "selmer.util")
312+
log* (resolve 'clojure.tools.logging/log*)]
313+
(log* logger :warn nil message))
314+
(catch Exception _
315+
(binding [*out* *err*]
316+
(println "DEPRECATION WARNING:" message)))))
317+
318+
(def ^:dynamic *deprecation-warning-handler* default-deprecation-warning-handler)
319+
320+
(defn deprecated-key-lookup
321+
"Looks up a key in context-map, preferring the namespaced version (e.g. :selmer/async)
322+
but falling back to the non-namespaced version (e.g. :async) with a deprecation warning.
323+
The warning is only emitted once per key per JVM session."
324+
[context-map namespaced-key non-namespaced-key]
325+
(if (contains? context-map namespaced-key)
326+
(get context-map namespaced-key)
327+
(when (contains? context-map non-namespaced-key)
328+
(when (and *warn-on-deprecated-keys*
329+
(not (contains? @warned-keys non-namespaced-key)))
330+
(swap! warned-keys conj non-namespaced-key)
331+
(*deprecation-warning-handler*
332+
(str "Using " non-namespaced-key " in context is deprecated. "
333+
"Please use " namespaced-key " instead.")))
334+
(get context-map non-namespaced-key))))

test/selmer/core_test.clj

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,84 @@
479479
(= "<script src=\"/js/site.js\" type=\"application/javascript\"></script>"
480480
(render "{% script \"/js/site.js\" %}" {}))))
481481

482+
;; Tests for namespaced keys (issue #325)
483+
(deftest script-namespaced-async
484+
(is
485+
(= "<script async src=\"/js/site.js\" type=\"application/javascript\"></script>"
486+
(render "{% script \"/js/site.js\" %}" {:selmer/async true})))
487+
(is
488+
(= "<script src=\"/js/site.js\" type=\"application/javascript\"></script>"
489+
(render "{% script \"/js/site.js\" %}" {:selmer/async false}))))
490+
491+
(deftest script-namespaced-defer
492+
(is
493+
(= "<script defer src=\"/js/site.js\" type=\"application/javascript\"></script>"
494+
(render "{% script \"/js/site.js\" %}" {:selmer/defer true})))
495+
(is
496+
(= "<script src=\"/js/site.js\" type=\"application/javascript\"></script>"
497+
(render "{% script \"/js/site.js\" %}" {:selmer/defer false}))))
498+
499+
(deftest script-namespaced-type
500+
(is
501+
(= "<script src=\"/js/site.js\" type=\"module\"></script>"
502+
(render "{% script \"/js/site.js\" %}" {:selmer/type "module"})))
503+
;; Verify user's own :type key doesn't conflict when using namespaced key
504+
(is
505+
(= "<script src=\"/js/site.js\" type=\"module\"></script>"
506+
(render "{% script \"/js/site.js\" %}" {:selmer/type "module" :type :my-value}))))
507+
508+
(deftest script-namespaced-priority
509+
;; Namespaced key takes priority over non-namespaced
510+
(is
511+
(= "<script src=\"/js/site.js\" type=\"module\"></script>"
512+
(render "{% script \"/js/site.js\" %}" {:selmer/type "module" :type "text/javascript"}))))
513+
514+
(deftest script-falsey-namespaced-key
515+
;; When :selmer/async is false, should NOT fall back to deprecated :async
516+
(is
517+
(= "<script src=\"/js/site.js\" type=\"application/javascript\"></script>"
518+
(render "{% script \"/js/site.js\" %}" {:selmer/async false :async true})))
519+
(is
520+
(= "<script src=\"/js/site.js\" type=\"application/javascript\"></script>"
521+
(render "{% script \"/js/site.js\" %}" {:selmer/defer false :defer true}))))
522+
523+
(deftest script-deprecated-key-warning
524+
;; Reset warned-keys so warnings are emitted fresh
525+
(reset! @#'selmer.util/warned-keys #{})
526+
(let [warnings (atom [])]
527+
(binding [*deprecation-warning-handler* #(swap! warnings conj %)]
528+
(render "{% script \"/js/site.js\" %}" {:async true}))
529+
(is (= 1 (count @warnings)))
530+
(is (str/includes? (first @warnings) ":selmer/async")))
531+
;; Second call should NOT warn again (once per key per session)
532+
(let [warnings (atom [])]
533+
(binding [*deprecation-warning-handler* #(swap! warnings conj %)]
534+
(render "{% script \"/js/site.js\" %}" {:async true}))
535+
(is (= 0 (count @warnings))))
536+
;; Reset and test defer warning
537+
(reset! @#'selmer.util/warned-keys #{})
538+
(let [warnings (atom [])]
539+
(binding [*deprecation-warning-handler* #(swap! warnings conj %)]
540+
(render "{% script \"/js/site.js\" %}" {:defer true}))
541+
(is (= 1 (count @warnings)))
542+
(is (str/includes? (first @warnings) ":selmer/defer")))
543+
;; No warning when *warn-on-deprecated-keys* is false
544+
(reset! @#'selmer.util/warned-keys #{})
545+
(let [warnings (atom [])]
546+
(binding [*deprecation-warning-handler* #(swap! warnings conj %)
547+
*warn-on-deprecated-keys* false]
548+
(render "{% script \"/js/site.js\" %}" {:async true}))
549+
(is (= 0 (count @warnings)))))
550+
551+
(deftest script-deprecated-key-warning-default-handler
552+
;; Verify the default handler writes to stderr (the println fallback path)
553+
(reset! @#'selmer.util/warned-keys #{})
554+
(let [sw (java.io.StringWriter.)]
555+
(binding [*err* sw]
556+
(render "{% script \"/js/site.js\" %}" {:async true}))
557+
(is (str/includes? (str sw) "DEPRECATION WARNING"))
558+
(is (str/includes? (str sw) ":selmer/async"))))
559+
482560
(deftest cycle-test
483561
(is
484562
(= "\"foo\"1\"bar\"2\"baz\"1\"foo\"2\"bar\"1"

0 commit comments

Comments
 (0)